Add a 'Fix' affordance on AI-generated images (hover pill on the message
thumbnail and an action in the fullscreen dialog) that attaches the image
to the composer as a reference file and seeds a 'Fix this image: ' prompt,
so the user only describes the fix.
Three attach paths, all reusing the existing file pipeline:
- Fix: attaches the generated image by reference (file_id, no re-upload) via
useAttachImage, injecting a completed ExtendedFile (attached: true) into the
composer file map — the send path forwards it verbatim as multimodal content.
- Drag-and-drop: plain image/file drops with no tool-resource choice now attach
straight into the current conversation instead of a single-option dead-end modal.
- Attach a previous image: new attach-menu item opens a picker of the current
conversation's images (collectConversationImages) and attaches by reference.
Pure helper collectConversationImages/resolveImageUrl unit-tested (10 cases).
i18n keys added to the default locale. No backend changes.
The content-free router-training feedback transport now lives once, in
`@hanzo/ai` `sendFeedback` (v0.2.1). `client/src/utils/rewardSignal.ts` becomes
a THIN adapter: it keeps the exact public `sendRewardSignal(requestId, signal,
rating?)` the message-actions and model-selector surfaces wire to, plus the
local `VITE_HANZO_FEEDBACK` opt-out, and delegates emission to the SDK with the
chat-specific `baseUrl` (VITE_HANZO_API_URL, cross-origin gateway) and the
end-user bearer lifted from axios defaults. The SDK owns the whitelisted
`{request_id, signal, rating?}` body, the dedupe, and the fire-and-forget
transport — so no prompt/response/code can transit, exactly as before.
The existing spec passes untouched (behavior preserved end-to-end through the
SDK; jsdom has no sendBeacon, so the SDK falls back to the same keepalive fetch
with the same headers).
Tests: client/src/utils/rewardSignal.spec.ts — 13/13 passed.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
Co-authored-by: hanzo-dev <dev@hanzo.ai>
Client-side product analytics via @hanzo/capture: pageviews, identify(user.id), and CHAT_STARTED/CHAT_MESSAGE_SENT with endpoint+model identifiers only — no message content or PII. Lockfile + tsconfig subpath mapping included.
Emit CONTENT-FREE reward signals to `POST {VITE_HANZO_API_URL}/v1/feedback` so
the router training loop gets production feedback. Payload carries ONLY
{request_id, signal, rating?} — never any prompt/response/tag/text/filename/code.
Fire-and-forget: never blocks UX, silent no-op on any failure.
The routing ledger keys each RoutingEvent on the upstream gateway response id
(chatcmpl-…/msg_…). The chat client previously held only locally-minted UUIDs,
so capture the REAL id server-side and thread it to the client as one field:
- server: ModelEndHandler captures `data.output.response_metadata.id ?? .id`
(the last CHAT_MODEL_END = the visible answer) into a shared `runMetadata`;
AgentClient surfaces it via `this.metadata` → `feedbackRequestId` on the saved
response message, riding the SSE final event + DB save to the client.
- schema: add optional `feedbackRequestId` to tMessageSchema (zod), IMessage
(mongo type) and the mongoose message schema (persists; SQLite keeps it in its
JSON doc blob automatically). Absent id ⇒ signal no-ops (never fabricated).
Client signals wired via new `client/src/utils/rewardSignal.ts`:
- thumbs up/down → up/down; regenerate → regenerate; copy → up (weak positive);
model-switch right after a response → switch (last assistant msg's id).
Dedicated cross-origin credentialed fetch (keepalive) to the Hanzo API — NOT the
same-origin chat backend — forwarding the end-user bearer for per-org attribution.
Honors local opt-out `VITE_HANZO_FEEDBACK=0|false|off` (server-side org/user
training opt-in is the preferred enforcement). 13 unit tests, all green.
Claude-Session: https://claude.ai/code/session_015Z1iLf7QBrq1LhignJrzDw
Co-authored-by: hanzo-dev <dev@hanzo.ai>
The controller required '@librechat/data-schemas', but this fork renames all
internal packages to @hanzochat/* (214 other files use @hanzochat/data-schemas;
that upstream name resolves to nothing). Runtime-only failure (require-time), so
the green build hid it and 0.9.21 CrashLooped at CloudUsage.js:1 MODULE_NOT_FOUND
— AFTER clearing the 0.9.21 zod/v4 fix. One-line name fix; all other requires in
the usage backend already resolve.
Any fresh chat image built after 74954c3f8 (@hanzo/iam 0.13 lockfile regen,
which pulled openai@6.46.0) crashes on boot:
ERR_PACKAGE_PATH_NOT_EXPORTED: subpath './v4' is not defined by
"exports" in openai/node_modules/zod/package.json
openai@6.46.0 declares zod '^3.25 || ^4.0' and imports zod/v4, but pnpm
mis-resolved its peer to the older zod@3.24.4 (which has no ./v4 subpath).
The deployed 0.9.19 image predates openai@6 entering the tree, so it never
hit this — 0.9.20 was simply the first rebuild to trip the latent landmine.
Fix: a single pnpm override zod@3.24.4 → 3.25.76 (already in-tree, a
backward-compatible superset that ships the zod/v4 compat subpath),
collapsing to ONE zod. Verified: no other package floated; openai@6.46.0
now pairs zod@3.25.76. Unblocks the whole chat build pipeline, not just
the 0.9.20 usage panel.
Adds the shared Hanzo usage read to LibreChat's Usage tab, beside (not
replacing) the Mongo token-credit view. Backend CloudUsage.js proxies
cloud's GET /v1/get-cloud-usages on-behalf-of (hanzo.id bearer resolved
server-side, never to browser); client renders the CloudUsageOverview
with @hanzo/usage's headless normalizeCloudUsage over native Tailwind.
Honest when unavailable: 200 {enabled:false} hides the section.
Brings the fork completion (de-librechat, @hanzochat scope rename, @hanzo/iam
0.13 API) and the org/project/user switcher (validated active-org -> X-Org-Id
on both cloud seams) onto main for release.
The account menu shows user -> org -> project and lets a multi-org member
switch their working org. The switch pins hanzo_active_org (httpOnly),
validated server-side against the caller's own membership set (owner + groups)
via POST /v1/chat/user/active-org. resolveActiveOrg() forwards it as X-Org-Id
on both on-behalf-of seams — the inference path (custom/initialize.ts) and
cloud agents (CloudAgentsClient) — where the gateway re-validates it in the
member's set (HIP-0026), so the header is a validated selection, never a trust
assertion. Adds Account/Console/Billing links. openidStrategy persists
organization/project/groups from the JWT claims; the user schema + TUser carry
them and getUserController serves them.
The static/IAM SPA path (client/src/utils/iam.ts, OAuthCallback.tsx) imported
BrowserIamSdk, removed when @hanzo/iam bumped 0.4->0.13 (HIP-0111); the class is
now IAM with an identical constructor config. pnpm-lock.yaml regenerated for the
@hanzochat rename. Fork now builds 5/5 tasks green.
- @librechat/agents -> @hanzochat/agents@^3.2.63 (published fork of
danny-avila/agents at github.com/hanzochat/agents; upstream remote kept
so agent-runtime fixes stay pullable)
- final sweep: zero @librechat/* package refs (chat internals + runtime)
- remove dead packages/agents residue (stale dist, no package.json)
- drop stale npm/bun lockfiles; pnpm-lock.yaml is the canonical lockfile
librechat-data-provider → @hanzochat/data-provider
@librechat/data-schemas → @hanzochat/data-schemas
@librechat/client → @hanzochat/client
(@hanzochat/api already forked; external @librechat/agents fork follows)
All imports, package.json names/deps, tsconfig path aliases, jest
moduleNameMapper, and rollup/vite build aliases updated. The data-provider
react-query subpath resolves via the exports map. Lockfiles regenerated in a
follow-on once the canonical package manager is confirmed.
True-black (client/src/style.css, client/tailwind.config.cjs):
- Converge the off-scale #0a0a0a onto the token scale. --gray-925 (consumed by
--presentation / --surface-primary-alt / --surface-dialog) and --green-950,
plus the Tailwind mono ramp's 950, now resolve to #050505, so inner panels /
dialogs match the elevated-surface value instead of reading as a lighter grey.
Verified in-browser: --gray-925/#050505, dark --surface-primary-alt/-dialog =
#050505, --surface-primary/-chat = #000.
- Root.tsx auth-loading fallback and LandingPage.tsx local tokens (bg #000 /
card #050505 / muted #171717) converged off #0a0a0a onto the same scale.
Overlay cap (client/src/mobile.css):
- Cap .nav-mask and .sidenav-mask at max-width:420px (anchored to the drawer's
own edge — left for nav, right for sidenav) instead of a full-width scrim, so
on a tablet the dim covers only the drawer footprint, not the whole viewport.
Align the nav-mask media query to 768px to match Nav.tsx's
useMediaQuery('(max-width: 768px)') so the cap also covers the tablet-portrait
boundary where the drawer is active. Verified: at 768px the scrim renders 420px
(55% of viewport), not 100%.
Touch targets (client/src/mobile.css, client/src/components/Chat/Input/ChatForm.tsx):
- Add a `composer-actions` marker on the composer action row and bump its round
action buttons (attach / tools / mic / send-stop, previously size-9 = 36px) to
a 44px min touch target on touch viewports. Verified 44x44 at 375px, unchanged
36px at desktop.
Build unblock (pre-existing dependency drift that hard-crashed the client):
- NetworkWallet.tsx: @hanzo/ui 5.x dropped ./network and ./wallet exports, whose
named imports failed the Vite dep-scan and Rollup build; render null with a
re-enable recipe (mirrors hanzo/app).
- utils/iam.ts: @hanzo/iam 0.4.x exports the SPA client as BrowserIamSdk, not
IAM; the wrong name crashed the app at mount and failed the build. Same
constructor config; swap to BrowserIamSdk.
Verified: `vite build` passes; Playwright at 375/768/1280 confirms true-black
surfaces, the 420px overlay cap, and 44px composer targets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SQLite DocModel (the Mongo replacement chat migrated onto) exposed
`distinct()` only on the model (static call sites like
`PromptGroup.distinct('category', filter)` worked), but `find()` returns a
`QueryBuilder` that lacked it. Every query-chain call site —
`AclEntry.find(q).distinct('resourceId')` (findAccessibleResources,
findPubliclyAccessibleResources) and `AgentCategory.find(q).distinct('value')`
— threw "find(...).distinct is not a function", crashing permission
resolution.
One-place DRY fix mirroring mongoose's `Query.prototype.distinct(field)`:
QueryBuilder.distinct(field) is chainable (so a trailing `.lean()` still
works) and delegates to the existing DocModel.distinct, keeping filter/tenant
scoping identical to `.find()`. No mongoose reintroduced.
Tests: added ACL `$or`+`$bitsAllSet` distinct + AgentCategory
`.distinct().lean()` cases to DocModel.spec.ts (14/14 pass).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neutralize the per-tool accent hues on the composer's active
CheckboxButton chips so the shell chrome recedes and stays true-black
monochrome (matching hanzo.ai). Each tool's "on" state painted itself a
distinct color — Search=blue, Code=purple, FileSearch=green,
Skills=cyan, Artifacts=amber (+ its split-menu button) — reading as a
rainbow in the composer. Unify to one neutral active treatment via the
existing semantic tokens (border-border-heavy + bg-surface-active-alt),
which already track the dark/true-black theme.
Also drops the stray text-cyan-500 on the queued-skills chip icon →
text-text-secondary.
Shell top-left (H mark alone + brand right-click menu) and bottom-left
(consolidated AccountSettings cluster) are already correct via the
shared @hanzo/ui HanzoHeader (v5.5.1) + AccountSettings — no fork.
No org switcher exists in chat (org is server-derived from the hanzo.id
owner claim); none fabricated. Semantic hues (balance status, errors,
online dots) left intact. Message content / syntax highlighting
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A rejected gateway key or an out-of-credits balance can end a `stream:true`
completion with NO content and WITHOUT throwing — api.hanzo.ai answers as a
200 event-stream that yields zero deltas — so the agent run "succeeds" with an
empty response and the client renders an EMPTY assistant bubble. This is the
exact silent failure behind the dead-guest-key GA blocker (a dead key -> 401 ->
{"msg":"invalid API key"} that the SSE client rendered as an empty reply).
Add a backstop in ResumableAgentController: right after `client.sendMessage`,
if the response carries no user-visible content and the run was not aborted,
throw. The throw lands in the existing catch -> `GenerationJobManager.emitError`
-> the `event: error` SSE frame the client already renders as an error bubble.
No new SSE plumbing — it reuses the one error path, so a dead key or exhausted
balance can never again be invisible.
The emptiness decision is a pure, unit-tested helper (`isEmptyAgentResponse`):
non-empty iff there is non-whitespace `text`, a non-blank text/think part, or
any non-text content part (error, tool_call, image, ...). An ERROR part still
renders, so the gateway-threw path is untouched.
Tests:
- emptyResponse.spec.js: 8 cases pinning the predicate (empty content array,
blank-text-only, error/tool_call/text parts non-empty).
- emptyResponseController.spec.js: drives the real controller — empty response
emits `error` (not a silent `done`); a response with content emits `done`.
Claude-Session: https://claude.ai/code/session_016yg7GPhYdWCh9vpp4HEwLZ
Co-authored-by: hanzo-dev <dev@hanzo.ai>
Model selector now surfaces the house family as "Zen": custom endpoints honor
their configured modelDisplayLabel as the group label, so the endpoint VALUE
stays "Hanzo" (existing conversations + pinned guest endpoint unaffected) while
the picker group reads "Zen". Config bumps the third-party providers to
customOrder 2+ so the built-in Agents endpoint (order 1) sits alone between Zen
(0) and the providers — Zen first, Agents second, other providers after. Does
not conflate Hanzo (agents) with Zen (models).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Accept ?project=<slug> (the org-unique key of the ONE cloud /v1/projects store,
org from the IAM JWT). A slim ProjectBanner shows the active project and links
the same slug back to the hanzo.app builder and console.hanzo.ai; a new
conversation opened for a project seeds the composer with a short opener so the
assistant has project context. Slug validated against the store's grammar and
persisted for the session so the scope survives the /c/new -> /c/:id navigation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(chat): shared NetworkSwitcher + WalletMenu in the HanzoHeader
Puts hanzo.chat on the ONE hanzo.network standard (same model as the
hanzo CLI, console, desktop, app): @hanzo/ui/network + @hanzo/ui/wallet
via the npm alias @hanzo/ui -> @hanzo/ui-shadcn@^5.7.3 (the 5.x line
continued under its post-v8-rename name; existing imports untouched).
- Nav/NetworkWallet: the surface adapter — injected EIP-1193 wallet
(non-custodial, zero key material) pinned to the selected network env
(mainnet 36963 / testnet 36964 / devnet 36965 / local / custom).
- Mounted via the HanzoHeader headerRight slot; dark-scoped to match the
monochrome bar.
- tailwind: scan the shared components in node_modules; add the missing
popover/destructive tokens (fallback to background/foreground vars).
vite build green; tsc baseline unchanged (1014 pre-existing errors on
clean main, zero added — upstream CI gates vite build only).
* chore(chat): bump @hanzo/ui-shadcn 5.7.3 -> 5.7.4 (genesis-canonical chain IDs)
Picks up the corrected network set (testnet 36962, devnet 36964, local
1337). vite build green.
---------
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Mirror the web client's server-driven Smart Routing onto the mobile
surface. On boot (authed), fetch the SAME backend route the web client
uses — GET /v1/chat/routing-defaults — fail-soft to { available: false }
so any error keeps today's local-toggle-only behavior.
- lib/routing.ts: mirror of web resolveSmartRouting (pure) plus org
derivation helpers and a module cache for the non-React api layer.
- lib/settings.ts: smart-routing pref is now nullable (null === follow
org default; true/false === explicit user choice that wins).
- lib/api.ts: fetchRoutingDefaults() boot fetch; chat() resolves the
effective model from pref + cached org defaults.
- App.tsx: authed AppShell fetches defaults once, caches + holds state.
- Usage.tsx: toggle uses resolveSmartRouting; locks off with copy
'Disabled for your organization' when auto_routing_active is false.
Resolver is mirrored (not imported) across app boundaries, matching the
three web apps; web copy carries the jest coverage. Typecheck + build
green.
Co-authored-by: Hanzo AI <ai@hanzo.ai>
New conversations resolve smart routing from the org's server-driven
defaults (GET /v1/chat/routing-defaults proxying cloud's
/v1/get-routing-defaults on-behalf-of) with the local toggle as a
nullable user override (null = follow org default). Fail-soft: an older
cloud-api (404), network error, or non-ok wrapper behaves exactly as
today (local preference only). When auto_routing_active is false the
toggle is locked with honest copy.
Effective state is one pure function, resolveSmartRouting
(client/src/utils/endpoints.ts), unit-tested in endpoints.spec.ts.
Co-authored-by: Hanzo AI <ai@hanzo.ai>
Web: 'Smart routing' toggle in the Usage settings tab, persisted via the
smartRouting localStorage recoil atom. When on, new conversations on the
Hanzo endpoint default to model "auto" (gateway routes each prompt to the
best/cheapest capable model; billed as whatever serves it). Scoped to the
Hanzo house endpoint and only applied to defaults — an explicit model pick
or a non-Hanzo provider family is never rewritten. Message header surfaces
the served model (msg.model) when a routed convo echoes it. Docs link +
en localization strings.
Mobile: same toggle on the Usage screen, persisted to localStorage via a
small settings helper; flips chat between VITE_CHAT_MODEL and "auto" in
lib/api.ts.
Backend: GET /api/usage (requireJwtAuth) aggregates this user's prompt+
completion Transaction spend over today/7d/30d plus a per-model breakdown,
enriched with the org tier from CommerceClient. Response mirrors the
@hanzo/usage UsageSnapshot shape (providerId 'hanzo', totals, providerCost)
for one wire format across Hanzo products.
Frontend: new Usage settings tab beside Balance, reusing the UsageBar idiom;
useGetUserUsage hook follows useGetUserBalance; en localization strings added.
The api jest winston mock returned the raw transform fn from
`winston.format(fn)`, so any `redactFormat()` call ran `fn(undefined)` and
crashed at `info.level`. `@librechat/data-schemas`'s own `config/winston`
calls `redactFormat()` at import time, so every backend suite that pulls in
`createModels`/`logger` failed to run ("Cannot read properties of undefined
(reading 'level')" at data-schemas parsers.ts:63).
Make the mock faithful to winston's contract: `format(fn)` returns a factory
that yields a Format instance (`{ transform: fn }`) — it never invokes the
transform at construction. On the models/strategies/Config slice this turns
20 fail-to-run suites into 20 passing (tests running 174 -> 681); the
level-throw is fully gone. Remaining failures are unrelated pre-existing
issues (DB-backed model tests, azureOpenAI config, role permissions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The node:sqlite → better-sqlite3-multiple-ciphers driver swap made the store's
public type surface (SqliteHandle.db, DocModel.db, openDatabase) reference the
driver's shipped `Database.Database` type. That type is large and conditional-
generic-heavy (`prepare<...>` → `Statement<...>`). Because @hanzochat/api imports
@librechat/data-schemas' public types (253 files), the driver types entered api's
TS program and the checker's structural-relation pass exploded (recursiveTypeRelatedTo
/ instantiateType depth-limit storm): api's rollup type-check ran >27 min and CI
canceled it. Pre-swap, node:sqlite's DatabaseSync was absent from @types/node@20 so
it resolved to `any` — trivial surface, build completed in ~3 min.
Fix: declare the minimal better-sqlite3 surface the store actually uses
(exec/prepare/pragma/close, run/get/all) as a local `SqliteDatabase`/`SqliteStatement`
interface and use it in place of `Database.Database`; cast the lazy require to the
minimal ctor. Runtime is unchanged — the native handle satisfies the contract
structurally. Rebuilt data-schemas dist.
Local proof (Node 20):
- api tsc --noEmit: >27min/canceled -> 151s
- api rollup build (--max-old-space-size=6144, as Dockerfile): 98s, emits dist
- pnpm run frontend: completes end-to-end, exit 0
The prod hand-written model layer (api/models/*, separate from the data-schemas
methods) still constructed mongoose documents, which throw under the CHAT_STORE_SQLITE
flip where these models resolve to the SQLite DocModel/DualWriteModel:
- api/models/Transaction.js (createTransaction / createStructuredTransaction /
createAutoRefillTransaction): 'new Transaction(txData); ...; await transaction.save()'
-> a persistTransaction(txData, calculate) helper that runs the calculator FIRST
(it mutates rate/tokenValue/rateDetail in place, so ordering matters), strips the
non-schema calculator inputs (endpointTokenConfig/inputTokenCount/rateDetail) to
match mongoose's on-save strip, then Transaction.create(). Balance math, the
returned {rate,user,balance,[tokenType]} and result.transaction are unchanged.
- api/models/Role.js getRoleByName: 'new Role(defaults).save()' -> Role.create();
'.toObject()' is present on the created doc across all backends. The name-required
guard is untouched. Prevents 'Role is not a constructor' breaking RBAC self-heal.
Same bug class as the session P1; these are the actual prod paths (a DRY smell vs
the data-schemas methods — consolidation deferred). api-only; no data-schemas dist
change. The existing api Transaction/Role specs cannot run in this env due to a
PRE-EXISTING logger-mock harness break (reproduced on clean origin/main); the
identical persistTransaction pattern is proven green in data-schemas on both the
mongoose and SQLite/DualWrite stores.
Typing the store-resolved model as Model<IUser>/Model<ITransaction> (matching the
28 migrated domains) ballooned the inferred return of the methods that had NO
explicit return type — searchUsers, and getTransactions/deleteTransactions/
deleteBalances/createAutoRefillTransaction — past TS's serialization limit
(TS7056), so their factory .d.ts stopped emitting. Add explicit return types to
exactly those methods (pure type annotations + two no-op .lean() casts; zero
runtime change). Declaration emit is restored.
Rebuild the shipped dist (npm run build, exit 0): bundles + session/token/user
type declarations now carry the store-aware DataHandle signatures. Transaction
methods are internal (not in AllMethods), so they are tree-shaken from the bundle
and their .d.ts is not part of the shipped surface.
Adds authStore.sqlite.spec.ts (isolated native-sqlite spec): the REAL production
factories run against both served shapes the flip produces —
1) DualWriteModel (SQLite primary + SQLite mirror), and
2) the real createModels() wiring under CHAT_STORE_SQLITE (DocModel served).
Covers the exact regression (createSession no longer throws 'Session is not a
constructor'): createSession → findSession(refreshToken|sessionId) →
updateExpiration → generateRefreshToken(rotate) → deleteSession →
deleteAllUserSessions → countActiveSessions; createToken → findToken →
updateToken → deleteTokens (verify/reset + email normalization); createUser →
findUser → getUserById → updateUser → deleteUserById + Balance seeding;
createTransaction (debit + non-schema-field stripping) / createStructuredTransaction
/ createAutoRefillTransaction → getTransactions / findBalanceByUser /
deleteTransactions. Asserts refresh tokens stay hashed and the served model is
never a mongoose Model. Registered in test/ci.mjs ISOLATED (native driver).
14/14 green; the 101 pre-existing mongoose specs for the touched files still pass.
Same store-bypass class as Session: transaction.ts constructed documents with
new Transaction() + .save() (latent P1 — breaks identically once Transaction is
SQLite-served); user.ts + transaction.ts resolved their model via mongoose.models.
Refactor both factories to take a DataHandle and resolve via handle.models.<Name>.
Transaction create/auto-refill/structured now persist via the bounded .create()
(persistTransaction), stripping the non-schema calculator inputs
(endpointTokenConfig/inputTokenCount/rateDetail) so the persisted document is
byte-identical to the mongoose path (which strips non-schema paths on save);
balance math + returned TransactionResult unchanged. User already used .create()/
.findByIdAndUpdate(); only its model resolution moves.
createMethods now hands the real DataHandle to all four (User/Session/Token) —
the storeHandle mongoose-cast hack is removed; every domain resolves the store
uniformly.
Cold logins have been failing since Session was flipped to the SQLite store
(CHAT_STORE_SQLITE): createSession threw 'Session is not a constructor' because
session.ts used the mongoose-document constructor (new Session()) + doc.save(),
which the DocModel/DualWriteModel served under the flip do not implement.
Refactor createSessionMethods + createTokenMethods to take a DataHandle (the same
seam the 28 migrated domains use) and resolve their model via handle.models.<Name>.
Session create/rotate now use the bounded Model API: createSession generates the
_id up front (handle.Types.ObjectId), signs the refresh token bound to it, and
persists via .create(); generateRefreshToken (rotation) persists via .updateOne();
updateExpiration via .findByIdAndUpdate(). Refresh tokens stay hashed (hashToken),
JWT claims + expiry math unchanged, no session fixation. Token methods already used
the bounded API; only their model resolution moves off mongoose.models.
storeHandle (mongoose-cast) in createMethods still feeds these factories unchanged
(it is structurally a DataHandle); the caller cleanup follows in the next commit.
The 15 specs that instantiate the native better-sqlite3 driver corrupt each
other under a shared jest worker: jest gives each spec file its own JS module
realm while the native addon is process-cached per worker, so cross-file state
breaks sibling in-memory databases (driver-agnostic — reproduces with mainstream
better-sqlite3; a compound json_extract UNIQUE stops firing). Prod is unaffected
(one realm, one shared handle for the process lifetime).
test:ci now runs test/ci.mjs: the normal coverage pass for everything except the
native-driver set (via testPathIgnorePatterns), then each of those 15 specs in
its own jest process, exiting non-zero on any failure. NOT --runInBand /
maxWorkers=1 (a single shared worker still corrupts). No test weakening, no skips.
Bump chat 0.9.15 -> 0.9.16 (patch).
sharedSqliteHandle() reassigned sharedHandle on a collection-key change without
closing the prior connection — a latent native-handle leak on any rekey. Close
it before replacing, and export closeSharedSqliteHandle() so storeRegistry.spec
(rekeys twice) and tenantIsolation.coverage.spec tear the handle down instead of
leaking it past the file.
NOTE: this fixes the real leak the review identified, but does NOT resolve the
cross-file store-spec flake. That flake is a separate, deeper issue (see report):
driver-agnostic (reproduces with mainstream better-sqlite3 too), isolated to the
store's create path, timing/heap-sensitive (any probe masks it), and NOT fixed by
closing handles/GC/statement-cache/wrapper-pinning. Every store suite passes in
its own process; production (single long-lived handle, one module realm) is
unaffected.
node:sqlite (DatabaseSync) is a Node 22+ builtin; prod runs Node 20 (Alpine),
so require('node:sqlite') threw ERR_UNKNOWN_BUILTIN_MODULE, cascading through
createModels → applySqliteOverrides → openDatabase and breaking the built dist
("createModels is not a function"). Swap to better-sqlite3-multiple-ciphers
12.11.1 (engines include 20.x, synchronous drop-in, SQLCipher AES-256 at rest —
the Node embodiment of the hanzoai/sqlite contract).
- data-schemas dep + root pnpm.onlyBuiltDependencies allowlist (native addon);
rollup externalizes the driver.
- openDatabase(): lazy require of the driver, identical WAL/synchronous/
busy_timeout/foreign_keys pragmas. Wire the CHAT_SQLITE_KEY encryption seam
(SQLCipher cipher/legacy/key pragmas applied before any page-touching
statement; 64-hex validated, fail-closed; key/path never logged). KMS/CEK
derivation is Milestone 2.
- DocModel: retype db to the driver's Database instance; binding/return parity
already correct (booleans→1/0, dates→ISO, TEXT reads only). engine untouched
(27/27 held).
- parsers: restore the structured-logging-context impl dropped in an upstream
merge (spec #13110 landed without its parsers.ts) — appendRequestContext for
non-debug lines and drop the __SYSTEM__ tenant sentinel from debug traversal.
- models: register the SystemGrant model in createModels (schema/model/methods/
SQLite-spec + coverage guard all existed; only the registration was dangling).
Scrub all node:sqlite/DatabaseSync references from src (comments + spec names).
The SQLite store's update path feeds attacker-influenced keys ($set/$unset/
$inc/$push/$addToSet — conversation import, saveConvo, agent metadata) straight
into setPath, which walked cur['__proto__'] / cur['constructor'] and could mutate
a shared prototype (global prototype pollution). Reject any dotted path whose
segments include __proto__/prototype/constructor (and the single-segment own-key
case a JSON.parse'd body produces). Legitimate nested writes are unaffected.
engine.spec: +5 guard cases (dotted $set, constructor.prototype, JSON-sourced
own __proto__, $setOnInsert/$inc, legit nested still writes). 27/27 green on
Node 20 (pure-JS engine, no node:sqlite dependency).
The pnpm workspace does not hoist a bare @librechat/data-schemas symlink to
the app root where the backfill runs (node config/backfill-sqlite.js), so a
bare require throws MODULE_NOT_FOUND in the pod. Fall back to the workspace
path (packages/data-schemas). Verified: require('/app/packages/data-schemas')
resolves in the chat pod.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Mongo→SQLite cutover, completed so chat-docdb can be DELETED with zero
data loss and an instant revert at every step.
- DualWriteModel: wraps a primary (served) + mirror model; reads/props fall
through to primary, the 10 write methods run on primary then replicate the
affected docs to the mirror KEYED BY THE PRIMARY'S _id (upsert if present,
delete if gone). Symmetric — same code mirrors mongoose->sqlite (pre-flip)
and sqlite->mongoose (post-flip escape hatch). Mirror failures are logged,
never thrown, so the served path can't break; gaps are caught by the
pre-flip count reconcile + the idempotent backfill.
- DocModel.upsertRaw: exact by-_id upsert (verbatim, no timestamp stamping) —
the shared primitive for the mirror AND the backfill, so live mirroring and
the one-shot copy converge on one keyspace without duplicating.
- Seam: applySqliteOverrides now honors TWO flags — CHAT_STORE_SQLITE (served)
+ CHAT_STORE_DUALWRITE (mirrored) — yielding the four cutover states. One
shared node:sqlite connection per process (was per-createModels-call).
- Route User/Session/Token through the handle (methods/index.ts) and add the
auth+billing CollectionSpecs (User/Session/Token/Balance/Transaction). These
are the collections actually populated in chat-docdb outside the 24 already
wired; User is the hot-path record every request loads and every conversation
references by _id, so it MUST move for a lossless Mongo delete.
- connectDb() is now Mongo-optional: unset MONGO_URI => SQLite-only mode, skip
the connection (final state, chat-docdb gone) with bufferCommands off so a
stray mongoose query fails fast instead of hanging.
- Dockerfile{,.multi,.static}: node:20/22-alpine -> ghcr.io/hanzoai/nodejs
:v24.18.0 (Node 24; node:sqlite built in, better-sqlite3 compiles).
- config/backfill-sqlite.js: one-shot Mongo->SQLite copy + count reconcile.
Tests: DualWriteModel.spec (10) green — mirror-by-primary-_id both directions,
read passthrough, bulkWrite, idempotent backfill, all 29 specs build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): raise cloud-agent run timeout 30s→180s (env CLOUD_AGENT_TIMEOUT)
A cloud agent run is a real chat completion — a zen5-mini answer routinely
takes ~25-30s (measured 28s), larger models/prompts longer. The hardcoded 30s
client timeout aborted long runs mid-flight, surfacing in the UI as a 502 even
though the cloud run finished and was recorded. Bump the default to 180s and
make it env-configurable (CLOUD_AGENT_TIMEOUT), matching the existing
CLOUD_AGENT_MAX_CONCURRENT knob. List/get are fast; this headroom only affects
/run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(agents): lock cloud-agent run timeout contract (180s default + env override)
Adds coverage the timeout bump lacked: asserts the 180s default (the fix for
the 30s -> in-UI 502), that an explicit constructor timeout wins, and that
CLOUD_AGENT_TIMEOUT overrides the module-load default (isolated re-require).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The unpinned depcheck (workflow installs latest each run) shifts which
@opentelemetry/* packages it reports: it flagged only @opentelemetry/core
before, now also exporter-trace-otlp-http and sdk-trace-base. All three are
transitive deps of @opentelemetry/sdk-node (imported/driven via NodeSDK in
packages/api/src/telemetry/sdk.ts), pinned top-level for otel version
alignment, none imported by name. Ignore the complete set so ROOT_UNUSED is
empty regardless of depcheck's version-dependent otel detection. Only
@opentelemetry/api is imported directly and stays checked.
Verified locally: with the three ignored, depcheck's root output contains only
@opentelemetry/api, which the workflow's used-in-code list subtracts -> empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
detect-unused-packages and detect-unused-i18next-strings each hard-fail on the
comment step because the ARC runners have no `gh` CLI (`gh api` -> exit 127), and
detect-unused-packages additionally flags @opentelemetry/core.
- Add .depcheckrc.yml ignoring @opentelemetry/core: it is a transitive
requirement of the otel stack in use (sdk-node / sdk-trace-base /
exporter-trace-otlp-http), intentionally version-pinned at the top level and
never imported by name, so depcheck reports a false positive. Verified locally:
with the ignore, depcheck no longer lists it (it was the sole ROOT_UNUSED item).
- Move both workflows' "post comment" step from `gh api` to
actions/github-script@v7 (Octokit + built-in token; jobs already grant
pull-requests: write). No dependency on a gh binary that isn't on the runner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Genuinely unused: defined only in en/translation.json, referenced nowhere in
code (the sibling com_agents_cloud_* keys are all used; this section-header
string never shipped a consumer). Its presence was the sole finding of the
detect-unused-i18next-strings workflow, which hard-fails on any unused key.
Removing it makes that check pass without weakening it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two pre-existing, repo-wide CI failures that reddened main and every PR:
1. test.yml / e2e.yml OOM'd building packages/api (rollup peaks ~5 GiB RSS;
Node's default ~2 GiB old-space heap => "heap out of memory", exit 134).
Add the same NODE_OPTIONS heap knob backend-review.yml/frontend-review.yml
already use (6144, well under the 8 GiB runner limit). Verified: build:api
completes in ~54s at 6144 (peak RSS 4.98 GiB); it OOMs at 3072.
2. test-linting.yml ("LiteLLM Linting") lints a litellm/ Python dir that does
not exist in this repo and runs `poetry install` with no pyproject.toml, so
it always failed at "Install dependencies". Pure upstream LibreChat/LiteLLM
residue — the real JS lint is eslint-ci.yml ("Run ESLint Linting"), which
passes. Remove the dead workflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Node >= 22.5 ships `node:sqlite` (DatabaseSync), which the SQLite document
store requires once CHAT_STORE_SQLITE / CHAT_STORE_DUALWRITE is enabled.
Dockerfile.static was already node:22-alpine; this brings the runtime image
to parity. The store lazy-requires node:sqlite (stores/sqlite/index.ts), so
with the flag unset the runtime boots unchanged — this is a pure Node bump,
no SQLite enabled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): restore drifted ~/auth SSRF/allowedAddresses helpers
The tests + callers (hardenedFetch, MCPOAuthHandler, MCP connection) already
expected the port-scoped allowedAddresses SSRF surface (LibreChat #12933/#13022),
but domain.ts/agent.ts had been reduced to the pre-#12933 versions while their
specs were trimmed to match — a tests-without-impl drift babel hid at test time.
Restore the upstream pair (matched impl + spec):
- domain.ts: isAddressAllowed; 3-arg resolveHostnameSSRF/isSSRFTarget (allowed
host:port exemption); isOAuthUrlAllowed; validateEndpointURL; port-scoped
isMCPDomainAllowed fail-closed on unparseable allowlisted URLs.
- agent.ts: createSSRFSafeUndiciConnect(allowedAddresses, port) + allowedAddresses
connect-time exemption (fixes the dead 2-arg call in hardenedFetch.ts).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): restore circuit-breaker config + OAuth reconnection cooldown
mcpConfig gains the CB_* connect/disconnect circuit-breaker knobs and
OAuthReconnectionTracker regains its progressive cooldown (5m/10m/20m/30m capped)
that reconnection-storm.test.ts asserts. Both were trimmed while their tests
stayed at the upstream version.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): finish connection.ts transport port — proxy, response caps, WS SSRF
Completes the half-ported MCP connection/transport layer (LibreChat
#13076/#13219/#13224/#13274). The test suite (MCPConnectionSSRF,
MCPConnectionAgentLifecycle, reconnection-storm) was byte-identical to upstream
but the implementation stopped at the redirect-SSRF guard, leaving 22
tests-without-impl in MCPConnectionSSRF alone.
- Proxy support: serverConfig.proxy + PROXY/HTTP(S)_PROXY env with full NO_PROXY
semantics (wildcard, CIDR, IP-range, IPv6, host-suffix, port scoping); per-URL
ProxyAgent/Agent dispatcher selection tracked in this.agents; recomputed across
redirects; proxied-target SSRF preflight (IP literal -> resolveHostnameSSRF,
hostname -> allowedAddresses exemption or reject).
- Response-size caps: guardStreamableHTTPResponses opt-in wraps the body stream
with MCP_STREAMABLE_HTTP_MAX_RESPONSE_BYTES + MCP_STREAMABLE_HTTP_MAX_LINE_BYTES,
emitting a JSON-RPC error SSE frame instead of unbounded buffering.
- WS SSRF: resolveHostnameSSRF(host, allowedAddresses, port) now runs regardless
of useSSRFProtection (allowlist deployments), closing DNS-rebind to private IPs.
- Connect-lifecycle circuit breaker + agent cleanup (closeAgents) on disconnect.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(mcp): accept proxy + sseReadTimeout in MCP server config schema
Adds ProxyUrlSchema (http/https/socks, env-var resolved) to SSE/streamable-http
options and sseReadTimeout to the base schema so connection.ts's proxy/idle-read
support is reachable from real config. proxy is admin-only: z.never() in the
UI/API user-input schema.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci(chat): gate MCP connection/transport suite in the primary test workflow
Root cause of the drift: test.yml (the Hanzo-active CI) only ran 'test:api'
(the api/ Express dir) and 'test:client' — packages/api tests never ran here, so
the connection.ts port could drift from its byte-identical tests unnoticed.
Add packages/api 'test:transport' (MCPConnection*, MCPRedirectSSRFGuard,
reconnection-storm, MCPManager, auth domain/agent SSRF specs, hardenedFetch,
OAuthReconnection*) and run it in test.yml. A future half-port of the transport
layer now fails CI loudly. No .skip's exist in these suites; nothing un-skipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Root cause of new chats never titling: the title path invokes the model
NON-streaming (stream:false). The Hanzo Cloud gateway returns a valid
{choices:[{message}]} body, but the pinned langchain ChatOpenAI parses that
non-streaming body to ZERO generations, so invoke() throws
'Cannot read properties of undefined (reading \'message\')' and no title is
saved. Every agent RUN already streams (SSE), which parses the same gateway
correctly — that is why generation works but titles did not.
Force clientOptions.streaming = true in #titleConvo (after the omitTitleOptions
filter, which strips 'streaming'). Verified against the live gateway with Dave's
per-user key: streaming=true -> clean title; streaming=false -> the crash.
Pairs with the zen5-flash repoint (fast, non-reasoning): streaming fixes the
parse for all models; zen5-flash keeps the call under the 45s title timeout
(zen3-nano/qwen3-8b took ~60s).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New chats didn't reliably auto-title: zen3-nano maps to the reasoning model
qwen3-8b (live upstream), which took 16-31s per title — routinely near the
45s #titleConvo timeout. The other 6 families pointed titleModel at
third-party names (llama-3.1-8b, qwen3-coder-flash, ...) that are NOT in the
live gateway catalog, so they returned a 200 error-envelope.
Repoint every family's titleModel (and the Hanzo summaryModel) to zen5-flash:
a fast, NON-reasoning, in-catalog model (live upstream deepseek-4-flash) that
returns a clean single-line title in ~1-2s. Picked by direct api.hanzo.ai/v1
probes: zen5-flash ~1.5s clean; zen3-nano 16-31s; zen5-mini 10-20s; zen5 /
zen3-vl >40s; zen5-nano not servable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Hanzo Cloud gateway answers some failures with HTTP 200 + a JSON
error-envelope ({status:"error", msg}) that has no `choices`. On the
title path the OpenAI client parsed that 200 to `undefined` and
#titleConvo threw `Cannot read properties of undefined (reading 'message')`,
so new conversations never got a title.
Apply the same wrapHanzoGatewayFetch rewrite agent runs use, now explicitly
on the title client's fetch, so the envelope becomes a clean 402 the outer
try/catch skips gracefully. Export the wrapper from @hanzochat/api; the
re-wrap is idempotent (a second pass sees a 402, not a 200 envelope) and
response-only, so per-user hk- billing and normal completions are untouched.
Adds an idempotency unit test (7/7 pass).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The #titleConvo path no longer 404s (zen5-nano was off-catalog), but full
auto-title still needs a follow-up: zen3-nano reasons past the 45s title timeout,
and off-title-path models (e.g. llama-3.1-8b) return a 200 error-envelope the
title client mis-parses. Needs the gateway-envelope rewrite on #titleConvo + a
fast non-reasoning title model. librechat.yaml is the source the chat-config
ConfigMap is generated from (not used at runtime; CONFIG_PATH=/app/chat.yaml).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Custom endpoints resolve their icon via getIconKey -> 'unknown' -> UnknownIcon
(by endpoint NAME), not icons[custom] — so the earlier icons[custom] remap was
inert (reverted). The Hanzo house endpoint had no asset/iconURL match and fell
through to the generic lucide "bot". Now:
- 'hanzo'/'zen' -> ZenLogoIcon (ensō), matching the assistant message avatar,
on the welcome screen + model pill + picker menu.
- qwen/google-gemma/openai-gpt-oss -> real provider marks by name (the
KnownEndpoints enum lacks those keys). DeepSeek/Mistral logos unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Welcome heading: tracking-tight for crisp large-display type (Linear/Vercel).
- Code blocks: hairline-bordered true-black surface (rounded-lg, border-medium,
#0a0a0a) with a subtle bottom-bordered header instead of the heavier grey bar;
reads calm + monochrome now that code renders in Geist Mono.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Zen ensō (円相) replaces the generic lucide "bot" for the custom Zen endpoint:
welcome screen, model pill and menu icon now match the assistant avatar.
- Monochrome avatars: collapse the colorful DiceBear palette to a neutral grey
ramp (the green "GU" guest chip is gone), and neutralize the periwinkle
no-seed fallback in Icon/Avatar.
- Kill the stray "(" glyph: the right control-panel NavToggle handle was floating
mid-edge at 0.25 opacity; now invisible until hover (header controls remain).
- Model-picker menu separator used cool border-slate-*; now neutral border-light.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Hard-default to dark (true-black) unless the user explicitly picks light:
ThemeProvider getInitialTheme + index.html no-flash script now default dark,
and the loading canvas is true #000 (was blue-tinted #070b13). theme-color #000.
- Crisp type: font-synthesis:none + antialiased. Basel ships 400/500 only, so
faux-bold was blurring headings; hierarchy now comes from size + the real
Medium face. Code now renders Geist Mono (was forced to Consolas via !important).
- Auto-title: repoint titleModel/summaryModel zen5-nano -> zen3-nano (zen5-nano
isn't in the live /v1/models catalog, so #titleConvo 404'd and new chats never
auto-titled). zen3-nano is the known-good guest/test model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause: commit 46b636c8c0 (server-side anonymous guest chat) added
GUEST to the SystemRoles enum and stamped guest JWTs with role=GUEST, but
never added a roleDefaults[GUEST] entry nor seeded the role. Every guest
generation ran checkAccess -> getRoleByName('GUEST'), which missed the DB,
fell into the self-heal branch `if (!role && SystemRoles['GUEST'])`
(truthy) and called `new Role(roleDefaults['GUEST']).save()` ===
`new Role(undefined)` -> "Role validation failed: name: Path `name` is
required" -> the whole generation threw. Since the logged-out landing IS
the guest composer (0.9.3), nearly every visitor hit this.
Fix (three orthogonal parts):
- data-provider/roles.ts: add the missing roleDefaults[GUEST] (named,
mirrors USER's minimal grant; guest scope is enforced by
enforceGuestScope middleware, not by these permissions).
- data-schemas initializeRoles: seed GUEST at boot alongside ADMIN/USER so
it is deterministic, not lazily created.
- models/Role.js: gate the self-heal create on roleDefaults[roleName]
(possessing the canonical defaults) instead of SystemRoles[roleName]
(mere enum membership). roleDefaults entries always carry a name, so a
nameless create is now structurally impossible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uniform "build an app" entry across chat -> app -> console. One pure module
(utils/buildApp.ts) is the single source of the hanzo.app builder wire
(hanzo.app/dev?prompt=) and the `/build [prompt]` command grammar; every
surface funnels through it (DRY).
Phase 1 (ships): the hanzo.app handoff, reachable three ways —
- `/build [prompt]` slash command, intercepted in ChatForm.onSubmit
- "Build this as an app" action on assistant messages (HoverButtons)
- the inline preview pane's "Open in App" CTA
All open https://hanzo.app/dev?prompt=<encoded> in a new tab (noopener).
Phase 2 (scaffold): inline build mode. A `buildMode` recoil flag toggled by the
composer "Build an app" button (BuildAppButton) makes ChatView render a
stripped-down split — chat thread on the left + a side preview pane
(BuildApp/BuildPreviewPane) on the right. The pane is a placeholder whose CTA is
the Phase 1 handoff, seeded live from the composer text. A `/build` route
deep-links into build mode (seeds the composer from ?prompt=/?q=). When buildMode
is off, ChatView renders byte-identical to before (zero regression to normal chat
or the guest landing). Phase 3 (real inline codegen/preview) is documented inline.
9 unit tests for buildApp (url + command grammar). Guest flow untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SQLite DocModel store statically `import`s `DatabaseSync` from
`node:sqlite` at the top of `stores/sqlite/{index,DocModel}.ts`. That module
is re-exported by the data-schemas package index, which the API server loads
at boot — so the eager `require('node:sqlite')` in the bundled dist runs
unconditionally. `node:sqlite` only exists on Node >= 22.5; the runtime image
is node:20-alpine, so boot dies with
`ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: node:sqlite`
(crashloop) — independent of the store's inert-by-default flag.
The store is only ever exercised when `CHAT_STORE_SQLITE` is set (a CSV of
collections), via `createSqliteHandle` -> `openDatabase`. Make the import
lazy: `import type` for the erased type references, and a single
function-scoped `require('node:sqlite')` inside `openDatabase`. Module load
no longer touches the builtin, so the server boots on Node 20 with the store
inert; when the flag is set (on a Node >= 22.5 runtime) it works verbatim.
Proven: rebuilt dist has no module-scope require; loading dist/index.cjs with
`node:sqlite` blocked (simulated Node 20) succeeds, and openDatabase() still
defers to the builtin only when actually opening a database.
Unblocks deploying any main-based image (incl. the 0.9.5 guest-chat fix).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two anonymous-guest client bugs, both proven with live Playwright.
BUG A — guest can't SEND ("Unknown endpoint: Hanzo"):
`new QueryClient()` was built in App's render body, so every re-render
minted a fresh EMPTY client. A guest's expected 401s (mcp/servers,
files/config, keys?name=Hanzo, …) fire the queryCache `onError` →
`setError` → App re-render → the provider swaps in an empty client. The
lazy chat-form's `useChatFunctions` then reads `getQueryData([endpoints])`
off an endpoints-less client, so the custom `Hanzo` endpoint resolves with
`endpointType === undefined` and `parseCompactConvo` throws
"Unknown endpoint: Hanzo" — the completion POST never happens. Backend is
correct: guest token → /v1/chat/endpoints={Hanzo:{type:custom}},
/v1/chat/models={Hanzo:[zen3-nano]}. Fix: stabilize the client with
`useState(() => new QueryClient(...))` — one client, one cache, every
consumer (incl. the lazy chunk) shares the populated store.
BUG B — intermittent first-load /login redirect:
The axios 401 interceptor hard-`window.location.href`'d to /login whenever
a one-shot refresh yielded no token. On a cold visit the guest bearer is
still in flight (`isGuestSession()` false because there's no bearer yet),
so an early expected 401 bounced the visitor to /login, racing the
guest-acquire. Fix: only hard-redirect when a real *session* bearer is
actually present (`currentBearer() != null && !isGuestSession()`); an
anonymous cold-start with no bearer is left to routing/the login gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The axios 401 interceptor hard-redirected to /login whenever a one-shot
refresh yielded no token. A guest (anonymous preview) session carries a
{guest:true} JWT that is valid ONLY on the chat-completion route; every
other endpoint (/api/mcp/servers, /api/files/config, ...) answers 401 by
design. Those expected 401s bounced the guest to /login, wiping the guest
session in an infinite loop, so the landing never rendered the composer.
Derive guest-ness from the active bearer (isGuestSession) and skip the
hard redirect for guests — real users with a truly-expired session still
redirect. Fixes anonymous-guest landing on hanzo.chat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enables 'generate an image of X' to render a real image inline through the
DALLE3 agent tool pointed at the Hanzo gateway (/v1/images/generations):
- DALLE3.js: model is configurable (DALLE3_MODEL, e.g. zen3-image); DALL-E-3-only
knobs (quality/style) are sent ONLY for a dall-e model so the Hanzo image
backend never sees a param it would reject. Agent path already fetches the
result server-side and returns it inline (upstream host never reaches client).
- handleTools.js: dalle is now a custom constructor that injects the signed-in
user's PER-USER hk- key (resolveHanzoCloudKey) so image generation is metered
to them — mirroring the chat per-user key path. Guests keep the shared key;
an authed user whose key can't be resolved FAILS CLOSED (no shared-org spend).
Deploy also sets env DALLE_REVERSE_PROXY=https://api.hanzo.ai/v1/images/generations
and DALLE3_MODEL=zen3-image. Zen-brand model id only; upstream never surfaced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): pure Mongo-shaped query/update engine for SQLite store
Correctness core of the mongoose->SQLite migration seam: pure, I/O-free
matchesFilter / applyUpdate / projectDoc / sortDocs implementing the exact
Mongo operator subset the chat data methods use ($eq $ne $in $nin $gt/$gte
$lt/$lte $exists $regex $not $and $or $nor; $set $unset $setOnInsert $inc
$push $pull $addToSet). Date-aware comparison, mongo null/absent semantics.
21 unit tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): SQLite DocModel + handle backing the Model API on node:sqlite
DocModel presents the Mongoose Model-API subset the chat data methods use
(findOne/find/findOneAndUpdate/updateOne/updateMany/deleteMany/deleteOne/
countDocuments/distinct/create/insertMany/bulkWrite + chainable QueryBuilder
with select/sort/limit/skip/lean/deleteMany). Docs stored as JSON via node:sqlite
(stdlib, zero new dep); JSON1 expression indexes on unique/anchor fields; exact
mongo semantics delegated to the pure engine; date rehydration on read; no
mongoose, no tenant middleware (Conversation/Message are not tenant-plugged
upstream). createSqliteHandle() returns a mongoose-shaped handle the unchanged
method factories run against. 7 adapter tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): prove conversations+messages fully off mongoose on SQLite
- Decouple message.ts + conversation.ts from the mongoose package: factories now
take a structural DataHandle ({models}) satisfied by BOTH mongoose and the
SQLite handle. Only type-only imports from 'mongoose' remain; zero runtime
mongoose in the data path for this domain.
- engine: type-aware operand coercion so cursor pagination (String(Date) operand
vs Date field) compares chronologically, mirroring mongoose schema casting.
- convoMessage.sqlite.spec: 14 tests running the REAL createMessageMethods /
createConversationMethods against createSqliteHandle — save/upsert, get,
update, delete, deleteMessagesSince, cursor pagination, bulk, archived +
retention-visibility filtering, getConvosQueried, cross-collection deleteConvos,
deleteNullOrEmptyConversations, searchConversation. All green.
Isolates one unrelated pre-existing fork gap via jest mock: this tree's
librechat-data-provider never synced the RetentionMode enum (upstream #13049),
so message.ts/conversation.ts reference an undefined export on the mongoose path
too. Documented for separate fix.
42/42 store tests green (engine 21 + DocModel 7 + contract 14).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): registry-aware createModels + store exports (the seam)
createModels now applies per-domain backend selection via CHAT_STORE_SQLITE
(CSV of collection names -> SQLite DocModel). Unset default = pure mongoose,
live path unchanged; only collections with a CollectionSpec are overridable
(fails closed otherwise). Exports createSqliteHandle/DocModel/CollectionSpec/
DataHandle from the package index. node:sqlite + node:crypto added to rollup
externals. Package builds clean (rollup, EXIT=0); 45/45 store+registry tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(data-schemas): drop local build artifacts + shared-deps symlink from branch
Revert dist/* to base (CI rebuilds bundles deterministically — no local builds)
and untrack the packages/data-schemas/node_modules dev symlink. Source-only branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): Batch 2 — Preset, ConversationTag, SharedLink on SQLite
Migrate three self-contained, non-tenant-plugged chat-document domains onto the
DocModel store behind the same seam. Decouple their factories to DataHandle;
createMethods now passes a registry-aware handle to createShareMethods so the
CHAT_STORE_SQLITE flag flips Share (pattern-2) in the live path too.
DocModel extensions (all reusable for later batches):
- compound unique indexes (ConversationTag {tag,user})
- ObjectId-ref casting on write + cross-collection .populate() (SharedLink.messages -> Message)
- findByIdAndUpdate / findOneAndDelete; findOneAndUpdate is now a chainable
QueryBuilder (mutate mode) so .lean()/.select()/.populate() chain after a write
- schema defaults on insert (SharedLink.isPublic:true)
engine: mixed-update semantics (top-level fields fold into $set) + $pullAll.
Contract specs run the REAL createPreset/ConversationTag/Share methods against
createSqliteHandle. 60/60 store+contract+registry tests green; rollup build EXIT=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): Batch 3 — Project on SQLite + realtime-vs-storage finding
Project migrated (pattern-1, registry-aware): add DocModel.findById; CollectionSpec
with array defaults; contract spec mirrors api/models/Project.js exact ops
(getProjectByName upsert, $addToSet $each, $pull $in, updateMany $pull).
Prompt/PromptGroup deferred with rationale: they construct mongoose.Types.ObjectId,
use an aggregate $lookup/$unwind pipeline + populate + manual tenant/ACL
(accessibleIds: ObjectId[]) — need an aggregate primitive, not the mechanical
recipe. Categories read-path is already mongoose-free (getCategories is static).
REALTIME finding (verified codebase-wide): chat has ZERO Mongo change-streams /
tailable cursors / .watch(). Its realtime is SSE token streaming (sendEvent /
agent GenerationJobManager) tied to the generation request — application layer,
DB-independent. So NO migrated domain needs a DB-subscription replacement; plain
node:sqlite is correct for all. Hanzo Base realtime is reserved for future
DB-driven push (multi-device live sync, presence, collab sessions) — none today.
Documented at the storage-decision source (collections.ts).
63/63 store+contract+registry green; rollup build EXIT=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): Batch 4 — File, Key, PluginAuth, Banner on SQLite
Migrate four non-tenant storage domains onto the DocModel store. Decouple their
factories to DataHandle; createMethods passes the registry-aware handle to
File/Key/PluginAuth (pattern-2); Banner is pattern-1 (~/db/models).
- Key: encrypted roundtrip proven (updateUserKey encrypt -> getUserKey decrypt)
against SQLite; CREDS_KEY/IV set via jest setupFiles (runs before module load).
- PluginAuth: replaced a redundant `new Model().save()` else-branch with
Model.create (equivalent on mongoose, part of the shared Model API — DocModel
supports it). No behavior change.
- Realtime directive recorded: Conversation+Message are the Base-realtime cutover
targets ("Base for realtime, SQLite for storage"); all other migrated domains
are pure storage. Empirically chat has zero Mongo change-streams (realtime=SSE),
so the SQLite store is correct in the interim; Base swap is backend-only.
MCPServer deferred (constructs mongoose.Types.ObjectId + _id ObjectId cursor).
70/70 store+contract+registry tests green; rollup build EXIT=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): Batch 5 — tenant-aware DocModel variant + Config on SQLite
Add tenant isolation to the store, mirroring the mongoose applyTenantIsolation
plugin, gated by CollectionSpec.tenantIsolated (unlocks Config/Skill/SkillFile/
SystemGrant):
- scopeFilter: every read/write filter scoped to getTenantId() (SYSTEM bypasses;
no-tenant + TENANT_ISOLATION_STRICT=true fails closed) — wired at the single
candidates() chokepoint.
- stampTenant: inserts stamped with tenantId (create/insertMany/upsert).
- sanitizeTenantUpdate: update payloads cannot mutate tenantId (throws cross-tenant;
strips from $set/$setOnInsert/$unset/top-level) — wired into
mutateOne/updateOne/updateMany/bulkWrite.
Config migrated (compound unique {principalType,principalId,tenantId}); decouple
createConfigMethods to DataHandle; QueryBuilder.session() no-op (single connection).
Fix: anchorWhere binds booleans as 1/0 (node:sqlite rejects JS booleans; json_extract
returns 1/0) — hardens every boolean-filtered collection.
batch5 contract spec proves cross-tenant isolation with REAL createConfigMethods:
per-tenant stamping, identical principals isolated across tenants, tenant-scoped
list/find, no cross-tenant delete. 74/74 green; rollup build EXIT=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): Batch 6 — SystemGrant on SQLite + ObjectId read-coercion
SystemGrant migrated (tenant-plugged, same recipe as Config): CollectionSpec with
compound unique {principalType,principalId,capability,tenantId} + tenantIsolated;
add DocModel.exists(); decouple createSystemGrantMethods to DataHandle.
Add ObjectId read-coercion to the engine (coerceId): filters carrying real
mongoose ObjectId operands (SystemGrant.normalizePrincipalId casts USER ids to
Types.ObjectId) now compare against the hex strings the store persists (docs
stringify ObjectIds to hex; _ids are ObjectId-hex). Applied in comparable /
valueEquals / anchorWhere. This is the shared primitive that unblocks the
ObjectId-coupled domains (Skill/SkillFile, MCPServer, Prompt/PromptGroup).
Contract spec runs REAL createSystemGrantMethods with USER principals
(ObjectId path exercised end-to-end): grant/has(exists)/revoke, idempotent upsert,
platform-vs-tenant isolation. 77/77 green; rollup build EXIT=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): Batch 7a — ObjectId shim + MCPServer, Skill, SkillFile
Add handle.Types.ObjectId shim (thin: store _ids are already ObjectId-hex,
coerceId resolves comparison/storage): a 24-hex ObjectId class with toHexString/
toString/toJSON/equals/isValid, exposed as SqliteHandle.Types and typed on
DataHandle.Types. createMethods' dbHandle now carries mongoose.Types (real
ObjectIds coerce too).
- MCPServer (pattern-2): full real-method spec — create / findByServerName /
findByObjectId (findById + ObjectId operand) / byAuthor / update / delete +
unique serverName. Decouple models + Types to handle; wired via dbHandle.
- Skill/SkillFile (tenant-plugged): decouple models + Types to handle; storage
contract proven — compound unique, ACL _id-in-accessibleIds ObjectId filtering
via coercion, SkillFile upsert. Their 800-line ACL/validation method layer
rides on these ops (full harness = follow-up).
81/81 green across 10 suites; rollup build EXIT=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): Batch 7b — aggregate primitive + Prompt, PromptGroup
Add DocModel.aggregate supporting the bounded stage set the chat methods use
($match / $lookup / $unwind / $sort / $limit / $project), run in JS over
candidate docs; $lookup resolves the mongo collection name to a sibling model
(prompts -> Prompt) and joins via the shared engine. Fix deepCoerceIds: coerce
ObjectId-like values to hex BEFORE structuredClone in create/insertOne (clone
was stripping the shim's methods, serializing productionId as an object husk).
Prompt / PromptGroup migrated (pattern-1): decouple models + Types to handle;
CollectionSpecs with productionId/prompts refs. Contract spec proves the
aggregate primitive directly AND the REAL getPromptGroup end-to-end (casts _id
-> ObjectId, $lookup productionId -> Prompt, $unwind preserveNullAndEmptyArrays).
All storage-tier domains are now on the DocModel. 85/85 green across 11 suites;
rollup build EXIT=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): Skill/SkillFile full-method harness + findOneAndUpdate(new:false) fix
Close the Skill/SkillFile proof gap: real createSkillMethods with stub ACL deps
(PermissionService injected) — createSkill (validation + uniqueness), getSkillById,
getSkillByName(accessibleIds ObjectId ACL), updateSkill optimistic version bump,
deleteSkill (+ removeAllPermissions), and SkillFile upsert (new-vs-replace) /
getByPath / list. Skill + SkillFile now fully proven, not just storage-proven.
Fix mutateOne: findOneAndUpdate(new:false, upsert:true) now returns null on an
insert (no pre-image) and the old doc on update — mongoose semantics that
upsertSkillFile's atomic new-vs-replace fileCount detection depends on. Locked
with a direct DocModel.spec test. All prior findOneAndUpdate upserts use new:true
(unaffected).
89/89 green across 12 suites; rollup build EXIT=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): Batch 8a — 7 chat-native domains on SQLite
Migrate MemoryEntry, ToolCall, Assistant, Action, AccessRole, Role, AgentApiKey
onto the DocModel store (mechanical: no external subsystem owns them — cloud
/v1/agents is additive, there is no /v1/memory, and app-domain authz has no IAM
equivalent). Decouple all 7 factories to DataHandle; createMethods passes the
registry-aware handle to Role/Memory/AgentApiKey/AccessRole (pattern-2).
Adapt role.initializeRoles off the `new Role().save()` mongoose-document pattern
to the shared Model API (findOne/create/updateOne) — equivalent on both backends.
Contract spec runs the REAL methods: memory set/create(dup-throws)/list/delete,
toolcall create/get/byConvo/delete, assistant/action upsert+get+delete, accessRole
create/find/list/delete, role initializeRoles seeds + listRoles, agentApiKey
create/validate(hash)/list. 96/96 SQLite-spec tests green; rollup build EXIT=0.
(Pre-existing mongoose specs fail identically on base — fork gaps, out of scope.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(data-schemas): Batch 8b — Agent, AgentCategory, AclEntry, Group on SQLite
Migrate the ObjectId/aggregate/ACL chat-native domains. Decouple factories to
DataHandle (models + Types); wire AgentCategory/AclEntry/UserGroup through the
registry handle. Agent uses the ObjectId shim (version tracking, _id cursor);
AgentCategory uses the aggregate primitive; AclEntry uses bitwise permission
queries; Group links members by id.
Store primitives added/fixed (all reusable):
- aggregate $group (with $sum/$first/$last/$max/$min/$push/$addToSet) — powers
AgentCategory.getCategoriesWithCounts.
- bitwise operators $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear — powers
AclEntry.hasPermission.
- applyUpdate deepCoerceIds the upsert seed (ObjectId shims survived to storage
as hex, fixing findOneAndUpdate-upsert with ObjectId filter fields).
- array-safe index anchor: json_each(...) EXISTS replaces json_extract= so
{field: value} over an ARRAY field (Mongo array-contains, e.g. Group.memberIds,
tags, projectIds) hits the index instead of being excluded by the prefilter.
Contract spec runs REAL methods: agent create/get/update/delete, category $group
counts, acl grant/has(bitwise)/revoke, group create/find/addMember/getUserGroups.
100/100 SQLite-spec tests green across 14 suites; rollup build EXIT=0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(billing): chat→commerce debit wiring behind COMMERCE_WRITES (default OFF)
Step 2 of the money migration. Default OFF = current local-Mongo billing path
runs byte-for-byte; the Commerce-first fail-closed READ gate is unchanged.
CommerceClient (the previously-dead write methods, now correct):
- recordUsage now debits the billing SUBJECT (billingSubject(owner,email)) — not
the Mongo user id — with amountMicros (lossless micro-USD; commerce rounds to
nearest cent + records exact micros) + requestId (stable per-spend idempotency
key → no double-debit) + totalTokens/provider.
- _flushUsageQueue now passes X-Hanzo-Org per entry (was omitted → debits hit the
wrong tenant and never netted the balance the gate reads). Fixed.
- add deposit() for credits (POST /v1/billing/deposit).
Wiring (flag-gated, additive, fail-open):
- commerceWrites.js: COMMERCE_WRITES gate + recordCommerceDebit (never throws
into the spend path; local Mongo authoritative until cutover).
- createTransaction: after the local debit, ALSO record to commerce when the
flag is ON and the request threaded `subject` (tokenValue is micro-USD;
transaction _id is the idempotency key). Inert until subject is threaded + the
flag flipped.
Tests (executed, PASS): CommerceClient.spec — recordUsage builds the right body
(subject/amountMicros/requestId/totalTokens) + X-Hanzo-Org header, no _namespace
leak; the flag gate is OFF by default.
REMAINING (gated): thread `subject` into txMetadata at the spendTokens call
sites; wire credits (deposit/grantStarter); Step-3 live-verify; then retire local
writes behind the flag. Local Balance/Transaction writes are NOT retired.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(billing): revert chat→commerce debit — the gateway is the single debit authority
The COMMERCE_WRITES chat-debit hook (bf6302a72c, today) is a double-debit
footgun and is reverted. It contradicts the established, LIVE architecture:
- hanzoCloudKey.ts (module doc): every IAM user has one hk- Cloud key; "the
cloud gateway (api.hanzo.ai) debits that key's org commerce balance and
returns 402 when the org runs out. Forwarding the right per-user key ===
correct per-user billing automatically." initialize.ts forwards that key on
every authed request (baseURL api.hanzo.ai/v1).
- packages/api usage.ts (NOTE, 2026-06-27): a prior chat→commerce write in
recordCollectedUsage was a SECOND debit to a mis-keyed account; it was
REMOVED. "Chat must NOT also record usage to Commerce."
- prod CR universe/.../crs/chat.yaml: HANZO_PER_USER_KEY=true, balance gate
off, and in its own words "chat records NO usage to commerce (that write was
removed); the single debit is still cloud's, per the user's hk- key."
Two writers to one ledger for one spend = double charge. The gateway is the ONE
debit authority for AI spend across every product (chat/code/agents/API); a
per-client debit path is the wrong DRY seam. So chat stays a READER of Commerce.
Removed (the whole unused chat→commerce WRITE surface):
- api/models/commerceWrites.js (the COMMERCE_WRITES gate + recordCommerceDebit)
- the recordCommerceDebit call + import in Transaction.js (now a comment
stating the single-debit invariant)
- CommerceClient.recordUsage (the debit) + its now-dead _usageQueue /
_flushUsageQueue machinery + interval; deposit/grantStarter (unused credit
helpers — the real first-chat grant is resolveHanzoCloudKey's direct POST
/v1/billing/grant-starter in packages/api, the correct layer)
- CommerceClient.spec.js (only tested the removed surface)
Kept (CommerceClient is now purely read-only, one responsibility):
checkBalance (fail-closed money gate), getTierConfig, isModelAllowed,
getCreditBreakdown — all live (balanceMethods.js, Balance.js).
Verify: node --check clean on both edited files; zero dangling references to the
removed symbols repo-wide; balanceMethods.spec mocks only the kept READ surface.
Money domain off-Mongo status after this: Commerce (via the gateway) is the
balance/debit authority — no Mongo Balance/Transaction WRITE is authoritative in
prod (gate off). The local Transaction doc remains an in-app usage log only;
retiring/relocating it to the SQLite store is tracked with the cutover.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(identity): User is a thin IAM projection — drop the local password credential (step a)
Identity migration step (a): IAM (hanzo.id) owns identity; chat's User doc is a
thin OIDC-keyed projection (provider='openid', openidId=userinfo.sub, org from
userinfo), never a second credential store.
Confirmed no OIDC path writes a password: openidStrategy.js (508-523) and
process.js createSocialUser build the User with NO password; createUser
(packages/data-schemas) writes no default; the schema field was select:false.
The ONLY writers were two local-email flows, both gated OFF in prod
(ALLOW_REGISTRATION=false, ALLOW_EMAIL_LOGIN=false).
Changes (source only — Docker rebuilds packages/data-schemas dist via
`pnpm run frontend`):
- schema/user.ts: remove the `password` field. The User projection carries no
local secret. (IUser keeps `password?: string` — always undefined — so the
disabled local strategy / comparePassword still compile; the full
local-strategy teardown lands with the cutover.)
- AuthService.registerUser: stop writing password (local signup stores no
credential).
- AuthService.resetPassword: reject — "Password reset is managed by Hanzo IAM
(hanzo.id)." Nothing to reset locally.
Login-safety: the authed OIDC flow (openidStrategy) references `password` ZERO
times — provably untouched by this change. comparePassword/localStrategy run
only when local login is enabled (off in prod), so removing the field is inert
for the live path.
Verify: data-schemas builds clean; store-registry + convo/message + user-schema
specs green (17/17 targeted); full suite identical to clean tree
(1210 pass / 250 pre-existing mongo-env fails / 100 skip — my change adds zero
failures). LIVE Dave-login verification is the deploy-time gate in the cutover
runbook — this branch is not deployed, so prod is untouched (default path stays
pure-mongoose + password field until the cutover deploy).
STOP before step (b): OPENID_REUSE_TOKENS is the documented login-breaker
(chat.yaml: =true → /api/auth/refresh 403 at hanzo.id → login dead). Do NOT flip
until the IAM refresh-token fix lands and is live-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Two regex-form OUR-route references the string-based sweep couldn't reach:
- api metrics.ts PATH_NORMALIZATIONS: matcher regexes still /^\\/api\\/...
while replacements were /v1/chat/... — a half-migration. Real request
paths (/v1/chat/*) never matched -> high-cardinality routes (stream/status/
files/messages/convos/agents/tags/tools/sessions with IDs) unnormalized ->
Prometheus label cardinality blowup. Matchers -> /^\\/v1\\/chat\\/.
(Aligns with metrics.spec's already-migrated /v1/chat/X/#id expectations.)
- client ToolCall.tsx: regex extracting actionId from the Action OAuth
redirect_uri matched /api/actions/:id/oauth/callback; callback is now
/v1/chat/actions/... -> actionId parse failed -> Action OAuth UI broken.
OUR-route /api now ZERO in all forms (string, template, regex, cookie-path).
Blue's transform anchored on '/api/' (trailing slash) and missed 4 places
where the path ends exactly at '/api':
- client MarkdownComponents.tsx: chat-message file links built as
${origin}/api/files/... -> BROKEN (files now at /v1/chat/files).
Fixed base -> /v1/chat. [user-facing regression: file downloads]
- data-provider getDomainServerBaseUrl(): same ${origin}/api -> /v1/chat.
- api oauth/csrf.ts OAUTH_SESSION_COOKIE_PATH '/api' -> '/v1/chat': the
OAuth session cookie (24h CSRF fallback) was never sent to the relocated
/v1/chat/{actions,mcp} callbacks. [vector-6 miss]
- api metrics.ts path normalization '=== /api' -> '=== /v1/chat' (matches
sibling /images,/avatars pattern).
OUR-route /api/ now ZERO across mounts, fetches, base-URLs, cookie paths,
redirect_uris.
Red-team review of the /api/ -> /v1/chat/* migration found the sweep
over-reached into paths that are NOT the chat mount prefix:
- client/src/utils/resources.ts: REMOTE_AGENT 'copy API endpoint' URL was
mangled /api/v1/responses -> /v1/chat/v1/responses (double-prefix, no
route serves it). Canonicalize to /v1/responses (OpenAI-compat Responses
API). User-facing.
- data-provider actions.spec + openapiSpecs (scholar-ai.net, swapi.dev) +
api mcp.spec wss template: third-party API path fixtures wrongly swept to
/v1/chat/*. Reverted to /api/* (they represent EXTERNAL APIs, not chat
routes). Fixes 5 failing createURL/executor tests.
- Stale doc comments: useFavorites (/v1/user -> /v1/chat/user), tokens.ts
typedef ref.
OUR-route /api/ remains ZERO. Login/OAuth/SSE/CSRF paths unaffected.
ChatGPT-style anonymous preview: when ALLOW_GUEST_CHAT is on, a logged-out
visitor renders the real chat view (composer + starter cards + model picker)
and can send a message as a guest on the free Zen model, WITHOUT logging in.
Sign-in is only prompted after the free per-IP quota (402 GUEST_LIMIT).
Client (the missing wiring — server guest path was already complete):
- ChatRoute now renders ChatView for canChat = isAuthenticated || isGuest;
/api/models + /api/endpoints queries run for guests (guest-scoped config),
and the roles gate treats a guest as loaded (no agent access). Previously
ChatRoute hard-returned null for !isAuthenticated, so a guest got the shell
but no composer.
- useAuthRedirect now surfaces isGuest.
Abuse control (airtight, server-enforced):
- guestMessageLimiter + guestTokenLimiter now key on the REAL client IP via
utils/guestClientIp (Cloudflare CF-Connecting-IP, falls back to req.ip), NOT
the guest token — clearing cookies / incognito / minting a fresh token can't
reset the count. Shared Redis limiterCache holds it across replicas.
- guestLimiters env parsed as positive ints.
Prod runs GUEST_MESSAGE_MAX=2. Guests always use the shared capped HANZO_API_KEY
(per-user hk- billing is skipped for guest principals).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mcp): block private IPs when the connector resolves all addresses
undici's connect-time DNS lookup calls the SSRF-safe wrapper with
{ all: true }, so dns.lookup returns a LookupAddress[] rather than a
single string. The previous guard only inspected `typeof address ===
'string'`, silently failing open on the array shape — a hostname
resolving to a private/reserved IP would pass unchecked.
Normalize both shapes to a flat address list and reject if ANY resolved
address is private, mirroring upstream LibreChat's getBlockedLookupAddress.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mcp): re-check SSRF on every redirect hop, strip cross-origin creds
An MCP server URL is server-controlled. Our customFetch issued the
request with undici's default redirect:'follow', so a public MCP server
(which passes the add-time isMCPDomainAllowed check) could 301/302/307/
308-redirect the connection to an internal IP literal (169.254.169.254
cloud metadata, 127.0.0.1, in-cluster 10.x/172.16-31/192.168 services).
undici skips its connect-time DNS lookup for IP literals, so the
redirect hop reached the internal target unguarded — a live SSRF on the
multi-tenant chat surface (proven: a 301 to 127.0.0.1/latest/meta-data/
was followed).
Harden createFetchFunction to follow redirects manually (redirect:
'manual'):
- Only 307/308 are followed, up to MAX_REDIRECTS=5; 301/302/303 are
returned unfollowed (the MCP SDK rejects a bare 3xx).
- Every hop's target is re-validated with isSSRFTarget (catches IP
literals the connect lookup skips) + resolveHostnameSSRF (catches
hostnames resolving to private IPs); a blocked hop is not followed.
- Cross-origin hops strip credential headers (Authorization, cookie,
mcp-session-id, plus runtime/config secret header keys) so a bearer
token / session id never leaks to a redirect target's origin.
- Cross-origin hops pin an SSRF-safe connect dispatcher for the rest of
the chain, closing the allowlist-mode DNS-rebinding gap.
Mirrors upstream LibreChat MCP redirect SSRF hardening (PR #12931).
Redirect targets get no allowlist exemption by design.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(mcp): prove redirect SSRF guard rejects internal-IP targets
End-to-end test using the REAL isSSRFTarget/resolveHostnameSSRF
classifiers (the sibling MCPConnectionSSRF suite mocks ~/auth). Stands
up loopback HTTP servers and asserts a 302/307/308 redirect to an
internal IP literal is never followed — the redirect is issued
(entryHit) but the internal metadata endpoint is never reached
(internalHit stays false). Also pins the classifier: 169.254.169.254,
127.0.0.1, RFC1918, localhost, ::1 are SSRF targets; a public hostname
is not.
Reverting the connection.ts guard makes all three redirect cases fail
(internal endpoint reached), confirming the test exercises the fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Converge hanzo.chat onto the shared @hanzo/ui design language:
- Sidebar toggle glyph: replace the custom filled panel SVG with the
canonical lucide PanelLeft geometry (stroke-2, 24x24). Kept as the shared
`Sidebar` export so OpenSidebar/NewChat/ExpandedPanel all get the one
canonical icon with zero call-site churn.
- Typography: Basel Grotesk (UI/body/display/heading, self-hosted woff2/woff,
Book 400 + Medium 500) as tailwind `sans`; Geist Mono (code/data) as
tailwind `mono` via CDN. Replaces Inter / Roboto Mono.
Dark aesthetic already matches the target (true-black OLED #000 / #0a0a0a).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the live chat-config CM: house-brand Zen (default zen5-mini) plus the
independent third-party families the gateway serves, current-gen models only
(no sunset zen4). Same api.hanzo.ai/v1 gateway + per-user key → identical
Commerce metering across all families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Elevate the empty-state starters from flat chips to a refined 2-col card grid:
lucide icon in a rounded well, hover lift (-translate-y) + soft shadow + border
lightening, smooth 200ms ease-out, focus-visible ring, reduced-motion safe.
Uses existing theme tokens (surface-primary-alt / border-light / text-secondary)
so it tracks light+dark + the monochrome brand. Agent starters render in the
same grid (default icon).
Proper semver: 0.9.0 -> 0.9.1 (patch, visual refinement of the 0.9.0 feature).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings the true-black OLED dark theme (style.css), the Zen ensō avatar
(ZenLogoIcon replacing the generic robot on the Zen/custom endpoint), the
chat→api.hanzo.ai/v1 unification, and the multi-provider picker (Zen house
brand + open families) onto main for a published build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plain-model chats (e.g. zen5-mini) showed a bare empty state — LibreChat only
rendered conversation starters for agents/assistants. Add 4 curated default
starters as the fallback so every empty chat gets ChatGPT/Claude-style
suggestion chips, wired through the existing useSubmitMessage path (landing-only;
agents that intentionally omit starters are untouched).
Proper semver: v0.8.3-rc1 (rc, non-standard v-prefix) -> 0.9.0 (minor, feature).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Qwen3 and DeepSeek are the upstreams behind Zen's flagship tiers
(zen5-max→qwen3.5-397b, zen5-pro/flash→deepseek-v4-pro/deepseek-4-flash).
Listing a Qwen or DeepSeek family next to Hanzo/Zen reveals the Zen mapping —
the single most forbidden thing in the brand policy. Keep only genuinely
independent open-weight families Zen's flagships don't build on: Meta Llama
and Mistral. Final picker: Hanzo (Zen, default zen5-mini) + Meta Llama +
Mistral. Every model is served by api.hanzo.ai/v1/models and its DO upstream
is verified invocable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror librechat.yaml to the LIVE chat-config CM (v1.3.4) and add per-family
custom endpoints alongside Hanzo/Zen. Every endpoint is the same
api.hanzo.ai/v1 gateway carrying the per-user HANZO_API_KEY, so metering is
identical across families (the cloud billing gate). Hanzo stays customOrder:0
with the zen5-mini default.
Families listed are the ones the gateway can genuinely invoke today via
DigitalOcean GenAI (funded): Meta Llama, Mistral, DeepSeek, Qwen. OpenAI and
Anthropic branded resale is intentionally omitted — no funded key path exists
yet (direct OPENAI/ANTHROPIC keys unfunded; DO account lacks proprietary
access), so listing them would surface non-invocable models. Documented inline
for a one-block-each add once a funded key/enablement lands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dark theme pushed toward black per brand: main canvas #000, panels/sidebar
#050505/#0a0a0a, elevated controls (input, cards) #171717 — remapped the `.dark`
--surface-* + shadcn HSL block onto new near-black gray steps (--gray-925/950/975)
in one place (DRY; the scale, not per-component overrides).
Zen avatar: the custom endpoint (hanzo.chat is Zen-only — Hanzo AI zen* models)
rendered the generic LibreChat "custom" robot glyph because the custom branch
hardcodes CustomMinimalIcon and ignores the endpoint iconURL. Replaced with a
proper ZenLogoIcon (ensō — the single-brushstroke Zen circle, monochrome
currentColor so it reads on true-black), named 'Zen'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The logged-out landing page listed Zen's upstream families (DeepSeek R1,
Qwen3, Llama 4, Phi-4) in the "third-party models" grid and named DeepSeek/
Qwen in the 100+ Models blurb — a brand-policy leak (Zen must present as our
own family; no raw Qwen/DeepSeek/Kimi/Llama/HuggingFace names). Keep only the
genuine independent providers offered via the gateway (GPT-5, Claude Opus 4,
Gemini 2.5, Grok, Mistral Large, Command R+) and lead the blurb with the Zen
family. Ships on next chat image build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Investigate-before-ripping map of hanzo.chat as the cloud "chat view":
- Documents the ONE inference path (POST /api/agents/chat/Hanzo →
api.hanzo.ai/v1/chat/completions, per-user hk- key, SSE) plus code-exec →
/v1/exec, websearch → /v1/websearch, cloud agents → /v1/agents. No shadow
LLM backend; config.yaml/litellm is dead upstream residue.
- Flags the ONE real parallel store: LibreChat Mongo (convos/messages/presets/
prompts/users/balances/files/sessions). Go backend has casibase-named
persistence (/v1/get-chats,/v1/add-message) + a speced-but-unimplemented
chat/openapi.yaml (/v1/chat/convos|messages|presets). Kill path documented;
do NOT rip Mongo (data loss + dead chat) — coordinate with openapi.
- IAM-native status: prod passport OIDC → hanzo.id (client hanzo-chat), LIVE;
static SPA mode uses client app-chat (align) on dormant @hanzo/iam ^0.4.0.
- @hanzo/ui (Tailwind/shadcn, chat's stack) vs @hanzo/gui (Tamagui/Next15,
console's stack): unify via @hanzo/ui, not a framework-swap rewrite.
librechat.yaml: drop gpt-4o/claude from the Hanzo endpoint default so the repo
reference mirrors the authoritative prod ConfigMap — zen-only picker, no raw
upstream names (brand policy). One way.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Red-team hardening of the on-behalf-of decouple:
- cloud.js getUserCloudBearer: bind EVERY forwarded token (id_token AND the
access_token fallback, session or cookie) via isForwardableToken. Principal
binding is now MANDATORY (sub === req.user.openidId) with no fail-open when
openidId/sub is absent, and the access_token fallback is bound too — closes
the unbound-access_token and null-binding confused-deputy paths.
- AuthService.persistOpenIDTokensToSession: take the OIDC refresh credential
EXPLICITLY (not from the tokenset). The decoupled default persists only the
on-behalf-of bearer (id_token/access_token), NOT the OIDC refresh_token, so
logout keeps using the local cookie -> findSession matches -> the server-side
session is invalidated (was silently skipped once openidTokens landed in the
decoupled session). REUSE path unchanged (passes the resolved credential).
Tests: cloud.spec + AuthService.spec green (bound-access, opaque-401,
foreign-401, null-openidId-401, cookie-injection-401, decoupled-omits-refresh).
Pre-existing oauth admin-exchange arity failures untouched (out of scope).
/api/agents/cloud returned 401 "cloud agents require hanzo.id sign-in" for
EVERY signed-in user: getUserCloudBearer forwarded the id_token read from
req.session.openidTokens, which is only populated by setOpenIDAuthTokens —
called at login solely when OPENID_REUSE_TOKENS=true. The live deploy runs
OPENID_REUSE_TOKENS=false on purpose (true makes /api/auth/refresh use the OIDC
refresh-grant, which 403s against hanzo.id and breaks login), so the id_token
was never persisted and the cloud-agent run was dead on arrival. One flag
braided two orthogonal concerns: (a) persist the id_token for downstream
on-behalf-of calls, (b) use the OIDC refresh-grant on token refresh.
Decouple:
- AuthService: new persistOpenIDTokensToSession(req, tokenset) — the one writer
of req.session.openidTokens (server-side only, never a cookie). setOpenIDAuth-
Tokens now delegates its session write to it (DRY, behavior-preserving).
- oauth login: an OpenID login ALWAYS persists the id_token to the session,
regardless of OPENID_REUSE_TOKENS. With REUSE disabled the login still runs
setAuthTokens (local-JWT refresh) so login/refresh cookies are byte-identical;
only server-side session state is added. Persist is wrapped so it can never
break the login redirect. OPENID_REUSE_TOKENS now SOLELY gates the refresh-grant.
- cloud proxy: getUserCloudBearer keys off the VALIDATED principal
(req.user.provider === 'openid'), not the mutable token_provider cookie, so the
on-behalf-of decision is tied to identity. The forwarded id_token must also name
req.user (sub === openidId) and be unexpired — expired/absent yields an honest
401, never a fabricated run. Confused deputy denied at the identity layer.
Durable refresh of the ~1h id_token (hanzo.id/Casdoor OIDC refresh, or an
RFC-8693 token-exchange from the chat session) is a tracked FOLLOW-UP; the
login-breaking refresh-grant is NOT enabled.
Tests (jest): cloud proxy re-keyed to the principal + honest expiry/binding/
confused-deputy (20); oauth persists id_token when REUSE=false, uses the OIDC
writer when true, local user never persists, login survives a persist failure
(4); persistOpenIDTokensToSession session/expiry/no-session/no-tokenset (4).
Refresh path unchanged.
Note: oauth.spec's 2 admin-exchange tests are pre-existing red on origin/main
(spec expects a 7-arg generateAdminExchangeCode; source passes 4) — orthogonal
to this change, proven by a stashed baseline.
Red review of the cloud /v1/agents run proxy: core security (SSRF, CSRF,
token-exfil, wrong-principal, mount-order, injection) confirmed closed; three
gaps fixed:
- MEDIUM: the run proxy (a real billable cloud completion) escaped the throttle
guarding the sibling chat-completion path. Add per-user cloudAgentLimiter on
the whole /cloud router (CLOUD_AGENT_USER_MAX/_WINDOW), a process-wide in-flight
ceiling with fail-fast 503 (CLOUD_AGENT_MAX_CONCURRENT), and a 4 MiB buffered-
response cap (Content-Length fast path + streamed abort) so no single call can
pin the shared backend's memory.
- LOW: only forward OpenID tokens when the request itself is an OpenID login
(token_provider==='openid'); a local-JWT user with a stale OpenID session can
no longer run as that prior identity (confused deputy).
- LOW: cap input by UTF-8 BYTE length, not UTF-16 units, matching cloud's maxInput.
Tests: api 34 passed (was 25; +9: principal-guard, cookie-read, byte-cap,
response-cap x3, concurrency-cap x2). eslint clean.
Move the cloud handle-grammar guard to the route boundary via router.param,
not only inside CloudAgentsClient. A malformed/decoded :name (traversal,
null byte, CRLF, backslash, space) is now rejected with 400 before any client
call is constructed — defense at the boundary. Production was already safe
(the client rejects the same names), but the guard belongs at the entry point.
Adds boundary-validation tests proving decoded smuggles (../etc, ../admin,
..\evil, %00, %0d, %20) 400 without reaching the client. cloud route 14/14,
CloudAgentsClient 11/11 green.
Add cloud-agent RUN alongside the existing LibreChat-legacy agent builder
(which is untouched). The canonical registry is cloud /v1/agents; chat now
lets a signed-in user run their OWN cloud agents from the thread via a
/agent <name> [prompt] slash command and via the @mention picker.
Backend (server-side proxy, token never reaches the browser):
- CloudAgentsClient forwards the user's hanzo.id id_token as a Bearer to
cloud; cloud's SanitizeIdentity (HIP-0026) validates it and pins X-Org-Id
from the owner claim, so a user only reaches their own org. Not an open
proxy: fixed host from HANZO_CLOUD_URL (falls back to OPENAI_BASE_URL host),
three fixed endpoints, agent name validated against cloud's handle grammar
(traversal/SSRF guard), 128KB input cap, 30s timeout, fail-secure 401 on
missing token (no service-token fallback).
- /api/agents/cloud/{,:name,:name/run} router, requireJwtAuth (guests rejected),
honest error passthrough. Mounted before the legacy /:id route.
Frontend (DRY, one run path, reuses Mention/MentionItem):
- useRunCloudAgent renders the run in-thread; parseAgentCommand is the single
command grammar; /agent and @mention both funnel through it.
- Cloud agents surface in @mention (cloudAgent type) and a /agent popover.
Mobile: AgentConfig responsive padding + full-width wrapping error text +
stacked tool/action buttons; AgentPanel form min-w-0 to stop overflow <768px.
Convergence of chat's /api/agents CRUD onto /v1/agents is a later step.
Tests: CloudAgentsClient (11) + cloud route (7) + parseAgentCommand (8) green.
The index.html tracker tag uses %VITE_ANALYTICS_SITE_ID%, substituted by
Vite at build time. The var was never set at build, so the placeholder
shipped literally and the browser POSTed website id '%VITE_ANALYTICS_SITE_ID%'
to analytics.hanzo.ai/api/send -> 400. The CR only set NEXT_PUBLIC_ANALYTICS_SITE_ID
(runtime, wrong prefix + phase for a Vite build). Default the real registered
Hanzo Chat site id as a Docker ARG/ENV so the frontend build bakes it in.
vite-plugin-pwa's default navigateFallback is 'index.html', so the SW
binds createHandlerBoundToURL('index.html'). index.html was in globIgnores
(not precached) -> 'non-precached-url: index.html' -> the service worker
breaks and strands users on a stale shell after each deploy (they see
/api/* 401s until a manual SW clear). Precache it; registerType:autoUpdate
keeps it fresh on release.
Both Agent Builder capabilities were 🔑-gated because nothing was wired.
- librechat.yaml: add webSearch{} pinned to Hanzo's own /v1/websearch surface
— searchProvider=searxng (Hanzo metasearch), scraperProvider=firecrawl
(Hanzo Crawl), no reranker. NO external Serper/Tavily/Jina/Cohere. Add
endpoints.agents.capabilities so execute_code + web_search show active.
- .env.hanzo-cloud: the old CODE_EXECUTION_ENDPOINT/RUNTIME_API_KEY were DEAD
(LibreChat never reads them, and the path was /execute not /exec). Replace
with the real vars: LIBRECHAT_CODE_BASEURL=https://api.hanzo.ai/v1 +
LIBRECHAT_CODE_API_KEY (KMS). Add SEARXNG_INSTANCE_URL / FIRECRAWL_API_URL /
WEBSEARCH_API_KEY -> api.hanzo.ai/v1/websearch.
- .env.example: document the Hanzo-backend vars as the one way; note external
providers are intentionally disabled.
Validated against the LibreChat Zod configSchema: CONFIG VALID (searxng +
firecrawl + execute_code/web_search capabilities).
Co-authored-by: Hanzo AI <ai@hanzo.ai>
In the agents-centric chat UI ('My Agents'), sending a message with no agent
created/selected POSTed to the agents completion endpoint without an agent_id,
which canAccessAgentFromBody rejected with 400 'agent_id is required in request
body'. A missing agent_id on the agents endpoint is an ad-hoc (ephemeral) chat,
not an error — isEphemeralAgentId(undefined) is already true.
- canAccessAgentFromBody: drop the 400 branch; a missing id falls through the
existing ephemeral path (no per-agent ACL) to plain chat.
- agents/build.buildOptions: resolve a missing agent_id to EPHEMERAL_AGENT_ID so
loadAgent builds an ephemeral plain-model agent instead of returning null.
- Hermetic regression specs for both fix points (no Mongo/winston); update the
stale middleware test that codified the 400.
Logged-in user types 'hi' -> ephemeral agent on the configured default model ->
reply, with no agent build required first.
Co-authored-by: Hanzo AI <ai@hanzo.ai>
The Hanzo Cloud gateway (api.hanzo.ai) answers some failures -- most importantly a request for a premium model (e.g. zen5-mini) when the caller's balance is only the $5 starter credit -- with HTTP 200 and a JSON error envelope ({status:"error", msg}) that carries no `choices`. The OpenAI client treated the 200 as success and parsed the choices-less body to `undefined`, so the agent run threw the opaque "Cannot read properties of undefined (reading 'role')" (and the title call died on reading 'message') -- no AI reply rendered, the generation job expired, and the resume stream 404'd.
Wrap the custom-endpoint OpenAI client fetch (scoped to the Hanzo gateway) so that envelope is rewritten into a clean 402 carrying the gateway's actionable message. Successful SSE streams and normal completions pass through untouched; the request is never inspected, so per-user hk- billing is unaffected.
- packages/api: hanzoGatewayFetch response-only wrapper + wire into initializeCustom + unit tests
A fresh /c/new defaulted to the built-in `agents` endpoint with no agent
selected. All chat routes through POST /api/agents/chat/:endpoint; with
endpoint=agents and no agent_id the access middleware (canAccessAgentFromBody)
returns 400 "agent_id is required in request body" and the SSE stream never
starts — every first message failed with no AI response.
Root cause: orderEndpointsConfig gives built-in `agents` order 1 but defaults
every custom endpoint to order 9999, so the client's getDefaultEndpoint picked
`agents` first. The intended override — a custom endpoint's `customOrder` — was
silently dropped by loadCustomEndpointsConfig (it never emitted `order`), even
though orderEndpointsConfig already types the custom spread as `& { order?: number }`.
Fix: loadCustomEndpointsConfig now honors `customOrder` -> `order`, and the Hanzo
endpoint sets `customOrder: 0` so it outranks `agents`. A new conversation now
defaults to Hanzo, which routes as an ephemeral agent (provider=Hanzo) through
the proven per-user hk- key path (resolveHanzoCloudKey) — real AI response,
metered, no agent_id 400. Agents endpoint stays available for explicit selection.
- packages/api: honor customOrder, widen TCustomEndpointsConfig, + unit test
- librechat.yaml: customOrder: 0 on Hanzo (mirrored in universe chat-config)
New-account chat now works end to end, fail-closed, no shared-org bleed:
- resolveHanzoCloudKey computes the canonical per-user billing subject
(object.BillingSubject parity: 'owner/name' for the shared hanzo catch-all),
stamps it on req.user, and ensures the one-time $5 Commerce starter credit on
THAT subject (POST /v1/billing/grant-starter) BEFORE forwarding the user's
hk- key — so the gateway's first balance check sees $5 instead of 402-ing.
Idempotent (in-process + commerce tag-dedupe), best-effort (gateway enforces).
- CommerceClient: checkBalance/tier/breakdown key on the subject and derive the
X-Hanzo-Org namespace from it; replace the wrong-ledger createTrialGrant
(credit-grants, invisible to /billing/balance) with grantStarter (Deposit).
- balanceMethods gate keys on the per-user subject (req.user.billingSubject).
- Drop chat-side Commerce usage write (single debit = the gateway).
- Remove the dead createTrialGrant call in user.ts (never fired for SSO users,
wrong ledger).
The prior HEAD (4233c8760) was reverted in the preceding commit: it rewrote the
gate to forward the user's IAM JWT to commerce AND added a chat-side
spendTokens -> /v1/billing/usage decrement. With per-user hk- forwarding ON,
that double-debits (cloud gateway debits the org via the user's hk- key, AND
chat debits the same org via spendTokens). One money authority only: the cloud
gateway debits via the per-user hk- key; chat's balanceMethods is a READ-ONLY,
fail-closed pre-flight gate (service token + X-Hanzo-Org, the proven pattern).
This commit adds the one missing piece for the anon free tier: guests are
exempt from the balance gate. Guests hit checkBalance via BaseClient
(supportsBalanceCheck[custom]=true) with no billing org; under startBalance:0
the legacy local gate would block the free tier entirely. Their spend is
bounded instead by (1) the per-IP guest message limiter and (2) the separate,
small-capped, NON-exempt guest key (HANZO_API_KEY) the gateway 402s when empty.
Tests: fail-closed decision matrix (funded/insufficient/unavailable/
model_not_allowed/no-org) + guest exemption. 8/8 green.
Commerce is already IAM-native + multi-tenant; chat was sending a static
service token (not a JWT) so commerce 401'd and the balance check fell open =
unmetered AI. Rewire to forward the logged-in user's hanzo.id IAM JWT per
request and fail CLOSED.
- CommerceClient: drop the static COMMERCE_TOKEN singleton; every call carries
the user's IAM JWT (Authorization: Bearer). getMyBalance (fail-closed read),
grantWelcome ($5 idempotent), recordUsage (decrement). getIamToken/
getBillingOrg/computeUsageCents/recordCommerceSpend helpers.
- balanceMethods.checkBalance: IAM-native money gate, FAIL CLOSED — no token /
commerce-unreachable / below-min => refuse. Funded user passes. Self-heals a
new account by granting the idempotent $5 once and re-reading (a spent wallet
stays blocked — grant is tag-deduped).
- spendTokens / spendStructuredTokens: decrement the user's per-org commerce
balance, mirroring the local ledger cost; threaded from the agent spend sites.
- openidStrategy: grant the $5 welcome credit on new-account signup under the
user's IAM identity (synchronous, best-effort).
- Balance controller: surface the authoritative commerce balance (display).
- Tests: fail-closed/fail-open decision matrix + cost mirror (12 cases).
Requires OPENID_REUSE_TOKENS=true (chat CR) so the IAM JWT is on the request,
and COMMERCE_EDGE_AUTH=true + hanzo-chat audience (commerce CR).
hanzo.chat money path. Previously every authenticated chat ran on z's shared
hk- key (hanzo/z) against the shared 'hanzo' commerce org, plus a $5 local
startBalance per user and a fail-OPEN balance gate -> unbounded free spend.
- Per-user billing: resolve (mint on first chat) each authenticated user's OWN
hk- key from IAM by org+email and forward it to the gateway, which debits
THEIR org. New packages/api/src/endpoints/custom/hanzoCloudKey.ts; injected at
the single apiKey chokepoint in custom/initialize.ts. FAIL CLOSED: an authed
user whose key cannot be resolved is blocked, never falls back to the shared
key. The resolver also stamps the authoritative billing org back onto req.user
(OIDC 'owner' can be the Casdoor super-org 'admin', not the real billing org).
- Balance gate commerce-first + FAIL CLOSED + decisive (balanceMethods.js):
when commerce is configured and the user's org is known, the org's commerce
balance is authoritative (>= HANZO_MIN_BALANCE); insufficient/unreachable ->
block. No fall-through to local tokenCredits, so startBalance:0 cannot
false-block a funded user. CommerceClient balance read is now fail-closed
(throws on cold error) and per-org scoped (X-Hanzo-Org).
- startBalance: 0 (librechat.yaml): no free local credits; new users claim $5
at billing.hanzo.ai. Client shows a claim-credit link on empty balance.
Gated by HANZO_PER_USER_KEY=true (reuses OPENID_* client creds for IAM).
The dark theme tinted all panels/sidebar blue via --background/--card/--border/
--input/--muted/--accent at hue 222 (and ring #2563eb). Neutralized to 0%
saturation (grayscale) to match the Hanzo monochrome brand.
The monochrome rebrand changed the LandingPage `colors` object + login CTAs
but left 8 inline rgba(253,68,68,…) (#fd4444) accents — hero gradient,
"AI Chat Platform" badge border, Zen-models card border/bg + hover, and the
"Free credit" pricing tier border/bg/label. Convert all to rgba(255,255,255,…)
(preserving alpha), matching the existing white monochrome accents. The
terminal traffic-light dots (bg-red/yellow/green-500) already render grey via
the tailwind color-scale remap. Landing is now fully black/white/grey.
Replace the upstream LibreChat feather/quill brand fallback with the
official Hanzo ▼/H mark (currentColor, monochrome) across every agent and
endpoint icon path (Landing welcome via ConvoIcon→AgentAvatar, agent
avatars, message + minimal icons).
Collapse all Tailwind color scales (green/red/blue/sky/…/rose) to a single
neutral grey ramp so UI chrome is black/white/grey only; destructive
semantics keep one muted red via --text-destructive. Convert the green
Create/Save CTAs (Add-MCP-Server dialog, Agent + Assistant builders,
preset and API-key dialogs) and .btn-primary to the adaptive
white-on-dark primary button.
Set favicon/PWA assets to the canonical hanzo.app ▼/H set (favicon.ico
byte-identical to hanzo.app) and the manifest theme_color to monochrome.
Tests: 34 passed — Hanzo-mark fallback assertions updated.
client/package.json: ^0.4.0 -> ^0.13.1 (npm latest). 0.13.x moves the PKCE tx (state+verifier) from sessionStorage to localStorage so it survives the OAuth redirect, fixing login.
iam.ts: BrowserIamSdk class renamed to IAM. Import bare '@hanzo/iam' (not the '/browser' subpath): the client tsconfig uses moduleResolution=node, which cannot resolve the exports subpath for types; the bare entry (dist/index.d.ts) re-exports IAM and Vite still bundles dist/browser.js via the browser export condition. Constructor config (IAMConfig) is unchanged.
OAuthCallback.tsx: IAMToken fields camelCased in 0.13 (access_token -> accessToken); without this the callback never set the auth header.
Lockfile: pnpm-lock.yaml regenerated (authoritative per packageManager=pnpm@10.27.0 + Dockerfile 'pnpm install --frozen-lockfile'). package-lock.json/bun.lock left untouched (not consumed by any build or CI).
The CSP script-src was 'self' only, blocking the app's own
analytics.hanzo.ai/script.js and static.cloudflareinsights.com beacon, plus a
data: font and assets/silence.mp3 (console errors + broken assets on every
page load). Allow-list the two external script hosts in script-src/connect-src
and add font-src/media-src for data:/blob:.
The OIDC/model iconURL and OPENID_IMAGE_URL point at hanzo.chat/assets/logo.svg;
that asset was still a made-up 24x24 single-path H. Replace with the canonical
64x64 blocky-H (5 paths) byte-matching @hanzo/brand — matches what the live
0.7.12 image serves.
The composer hard-requires a handful of read-only bootstrap calls. Switch
exactly those to requireGuestOrJwtAuth and add a guest branch that returns
safe, scoped data — never a DB lookup, never another user's data:
- GET /api/endpoints → ONLY the guest endpoint (Hanzo)
- GET /api/models → ONLY the single guest model
- GET /api/user → ephemeral guest principal (no email)
- GET /api/convos → empty conversation page
- GET /api/user/settings/favorites → []
- GET /api/agents/chat/active → { activeJobIds: [] }
Every mutation and read-by-id route stays JWT-only and rejects guests with
a clean 401 (post jwt-strategy fix). Banner (optionalJwtAuth) now resolves
with no user for guests. Adds controller + full-chain integration tests.
Single source of truth in guestConfig.js for the ephemeral guest shape:
- buildGuestPrincipal(id): plain GUEST req.user (no DB id/email)
- buildGuestUser: safe /api/user payload (name 'Guest', no email)
- buildGuestEndpointsConfig: ONLY the configured guest endpoint
- buildGuestModelsConfig: ONLY the single configured guest model
requireGuestOrJwtAuth now builds its principal from buildGuestPrincipal
(adds name 'Guest'), keeping the guest shape DRY across auth and bootstrap.
The guest JWT carries a synthetic `guest_<uuid>` id that is not a Mongo
ObjectId. On any requireJwtAuth route the 'jwt' passport strategy called
getUserById(guest_id) → Mongoose CastError → done(err) → HTTP 500.
Detect the `guest: true` claim at the top of the verify callback and
return done(null, false) before any DB lookup. Guest tokens now fail the
strict strategy with a clean 401, and reach their scoped routes only via
requireGuestOrJwtAuth. Fail closed by construction.
The chat route guard redirected any !isAuthenticated user to /login on a
300ms timer, ignoring isGuest/allowGuestChat and beating startup-config
load — so logged-out guests never reached the composer. Wait for config,
and skip the redirect when guest chat is enabled or the user is a guest.
silentRefresh's guest fallback could run before startupConfig loaded
(allowGuestChat undefined) and was never retried, leaving logged-out
visitors on the landing page instead of the guest composer. Add a
dedicated effect that acquires the guest token once the flag is true.
When unauthenticated and startupConfig.allowGuestChat is set, acquire a
guest token on load and render the chat composer instead of the landing
gate. On a 402 GUEST_LIMIT response, surface GuestLimitDialog which
triggers the existing OpenID/hanzo.id login flow.
- useGuestAuth: acquires an ephemeral guest session via dataService.
- AuthContext: isGuest state; guest fallback in silentRefresh; userQuery
and silentRefresh disabled for guests (capability-scoped).
- Root: render chat for guests (auth-gated hooks stay disabled); mount
GuestLimitDialog.
- useResumableSSE: dispatch guestLimitReached on 402 GUEST_LIMIT.
Add an opt-in anonymous preview mode gated behind ALLOW_GUEST_CHAT
(default off). Guests get a per-IP free quota (GUEST_MESSAGE_MAX,
default 3) on the free Zen model via the Hanzo gateway endpoint.
- POST /api/auth/guest issues a short-lived guest JWT ({guest:true},
per-token random id) signed with JWT_SECRET; rate-limited per IP.
- requireGuestOrJwtAuth accepts guest tokens ONLY on the chat-completion
router; the standard jwt strategy rejects them everywhere else.
- enforceGuestScope pins endpoint+model to the free Zen endpoint and
strips agents/tools/files/spec/preset server-side.
- guestMessageLimiter enforces the per-IP quota via the shared Redis
limiterCache; returns 402 {type:'GUEST_LIMIT'} on exhaustion.
- Reserved chat subpaths (stream/active/status/abort) stay JWT-only.
- Expose allowGuestChat/guestMessageMax in /api/config; add GUEST role.
Tests: 27 unit/integration tests covering config, guest auth wrapper,
scope enforcement, quota, and the router chain.
run.processStream (@librechat/agents >= 3.1.52) treats the passed config as
owned and, in its post-stream cleanup, sets config.configurable = undefined to
break the AsyncLocalStorage -> LangGraph reference chain that keeps heavy graph
state (base64 images/PDFs) alive (agents dist/esm/run.mjs:239). Reading
config.configurable.hide_sequential_outputs AFTER runAgents() therefore threw
"Cannot read properties of undefined (reading 'hide_sequential_outputs')",
caught by chatCompletion and pushed as an ERROR content part -- the red
post-reply banner. The same throw aborted the post-stream finalize, which is
why auto-titling also stopped.
Hoist the read to before runAgents() (the value is a static agent property only
needed afterward by the deprecated Agent Chain output filter), so it no longer
depends on post-run config state. Matches upstream LibreChat's fix
(PR #11942, a0f9782e6).
Adds a regression test that reproduces the mutation (processStream nulls
config.configurable) and asserts no ERROR content part is produced, plus
coverage for the hide_sequential_outputs true/false filter behavior.
Co-authored-by: Antje Worring <worringantje@gmail.com>
After the session-handoff fix let authenticated users reach /c/new, the
chat Nav crashed the whole route with react-router-looking 'Invariant
failed' — actually an Ariakit invariant: useMenuButton(store||context,
false). Root cause: two @ariakit/react copies in the bundle — the app
(client, ^0.4.15 -> 0.4.23) and @librechat/client (packages/client,
^0.4.16 -> nested 0.4.21). DropdownPopup (from @librechat/client) creates
<MenuProvider> in one Ariakit instance; BookmarkNav's <MenuButton> reads
the MenuProvider context of the *other* instance -> empty -> invariant ->
ErrorBoundary 'Oops! Something Unexpected Occurred'.
Pin @ariakit/react to a single 0.4.29 (latest 0.4.x; satisfies both
^0.4.15 and ^0.4.16) via overrides + pnpm.overrides so app and
@librechat/client share one Ariakit module/context. Verified:
@librechat/client + client (Vite) rebuild green; lockfile now resolves a
single @ariakit/react@0.4.29 with no nested copy.
Co-authored-by: Antje Worring <worringantje@gmail.com>
The data-provider had been rewired to a non-existent api.hanzo.ai 'cloud
gateway' surface (/v1/get-account, /v1/get-chats, /v1/signin, ZAP WebSocket).
The deployed backend is stock LibreChat serving /api/* — so the SPA's silent
refresh hit GET /v1/get-account, got the index.html shell back (no token/user),
left isAuthenticated=false, and Root rendered the marketing LandingPage even
for fully OIDC-authenticated users. Every chat/model/convo call was equally
broken against the live backend.
- api-endpoints.ts: restore the LibreChat-native /api/* endpoint surface
(refresh -> POST /api/auth/refresh, user -> /api/user, convos -> /api/convos,
models/presets/prompts/config/endpoints, etc). Drop dead Vite import.meta
base-url branches (unused in deploy; also unblocks Jest/CJS) while keeping
same-origin <base>/relative resolution + setApiBaseUrl override.
- request.ts: refresh via POST /api/auth/refresh (native), keep withCredentials
so the httpOnly refreshToken cookie is sent; retain pk- key helpers.
- config.ts: EndpointURLs stream submit -> /api/{agents,assistants}/chat.
Verified: tsc clean, 666/666 data-provider tests pass (2 pre-existing
import.meta-broken suites now green), data-provider + api + client-package +
client (Vite) all build.
Co-authored-by: Antje Worring <worringantje@gmail.com>
The upstream LibreChat consolidation (#11830) deleted api/models/{Message,
Conversation,Preset,Prompt}.js and moved their logic into @librechat/data-schemas'
createMethods. A later conflict-resolution merge (73d4b423a) reverted the
data-schemas createMethods composition back to the pre-consolidation form (so it
no longer provides message/conversation/preset/prompt methods) but did NOT restore
the deleted api/models/*.js files. The result is a half-applied consolidation:
- api/models/index.js still requires ./Message, ./Conversation, ./Preset
- api/server/middleware/error.js, .../abortRun.js, .../fork.js, prompt access
guards, etc. still require('~/models/Message' | '~/models/Conversation' |
'~/models/Prompt')
- but those files no longer exist -> backend crashes at startup with
MODULE_NOT_FOUND (api/models/index.js:13 -> ./Message), crashlooping the pod.
Frontend build + jest passed because they never exercised this require chain.
Fix: restore the four model files from their last good pre-#11830 revision
(214252d2e, on this branch's own ancestry, so namespaces match HEAD:
@librechat/data-schemas, librechat-data-provider, ~/db/models), updating only the
@librechat/api import to @hanzochat/api to match the forked package (#36).
Verified locally with module-alias registered as the server does:
require('./models') loads (138 exports, all message/convo/preset present);
the full crash chain models -> banViolation -> logViolation -> cache ->
db/indexSync -> server/middleware/error resolves with no MODULE_NOT_FOUND;
@hanzochat/api still exports handleError/sendEvent/createTempChatExpirationDate/
escapeRegExp/sanitizeMessageForTransmit.
Co-authored-by: Antje Worring <worringantje@gmail.com>
chat is PUBLIC; GitHub blocks public->private reusable-workflow reuse, so the
delegation to hanzoai/.github startup-failed (0s) for every push. Replace with
the proven self-contained workflow that built 0.7.10 on release/v0.7.10:
runs-on the ARC runnerScaleSetName 'hanzo-build-linux-amd64' (not label arrays),
GHCR-only, amd64 (DOKS is all amd64), immutable sha-<sha7> + semver tags, no
floating tags, builds ./Dockerfile. Add branches:[main] so main pushes build.
Co-authored-by: Antje Worring <worringantje@gmail.com>
The dispatch-to-universe job (ubuntu-latest + repository-dispatch) caused a 0s
startup_failure. Match the proven hanzoai/bot caller exactly: one reusable
docker-build job, GHCR, self-hosted arcd, secrets: inherit. Universe CR is
bumped out-of-band (operator reconciles), so the auto-dispatch is redundant.
Co-authored-by: Antje Worring <worringantje@gmail.com>
docker-publish.yml referenced $env.DOCKER_IMAGE / $env.GHCR_IMAGE that were
never defined, so the prod 'Docker Release' build failed at metadata with
'invalid tag "docker.io/:main-amd64"' (empty image, Docker Hub, floating
branch tag). Replace the broken inline workflow with the canonical reusable
hanzoai/.github docker-build.yml: GHCR-only, immutable sha-/semver tags,
self-hosted arcd amd64+arm64, then dispatch to universe.
Dockerfile.multi omitted .npmrc, so its pnpm install used the isolated linker
and tripped TS2742 in packages/client (rc-input-number type not nameable under
.pnpm). Copy .npmrc so it uses node-linker=hoisted like the prod Dockerfile —
one linker config, honored everywhere.
Co-authored-by: Antje Worring <worringantje@gmail.com>
The Docker Release CI fails at the 'pnpm run frontend' step, so prod runs a
stale image. Three pre-existing breaks in the data-provider -> client -> client
build chain, fixed minimally:
1. data-provider rollup typechecked test specs. rollup-plugin-typescript2 runs
over the whole tsconfig program (include 'src/**/*'), and orphaned upstream
specs (config.spec.ts, cloudfront-config.spec.ts) import members config.ts
never exported (cloudfrontConfigSchema, allowedAddressesSchema, ...), so the
library build died on TS2305. Exclude **/*.spec.ts / **/*.test.ts from the
rollup tsconfig: the library build no longer typechecks tests (jest still
runs them separately). Correct separation: library build != tests.
2. With specs excluded, the next break surfaced: skills.ts referenced
import('../types').InvocationMode, a type defined nowhere, while the client
imports InvocationMode as a value from librechat-data-provider. Define the
missing string enum (auto|manual|both) in skills.ts and export ./types/skills
from the package root (it was never re-exported, so TSkill/InvocationMode
were unreachable by the client).
3. client/vite.config.ts was a botched merge: one defineConfig object with every
key duplicated. JS last-key-wins made the second copy effective, and that copy
referenced undefined identifiers (buildSourceMap, NODE_POLYFILL_SHIMS -> the
config crashed at load) and used rolldownOptions (ignored by Rollup, so
manualChunks never ran -> a single 13MB chunk that Workbox refused to
precache). Remove the broken duplicate block; the original config (working
rollupOptions.manualChunks + 4MB precache limit) is restored.
Verified: pnpm install --frozen-lockfile && NODE_OPTIONS=--max-old-space-size=6144
pnpm run frontend now exits 0 (the exact Dockerfile build step). Produces
packages/data-provider/dist, packages/api/dist (handleError exported), and
client/dist (32 chunks, sw.js).
Forks the internal @librechat/api workspace package (packages/api) to
@hanzochat/api, matching the app package @hanzochat/chat. De-LibreChats
the package name and puts its build under our control.
Fixes the live prod bug where the deployed chat image shipped a stale
@librechat/api/dist missing the exported handleError(), breaking every
error path with "handleError is not a function" and killing the SSE
stream. A clean build of @hanzochat/api produces dist/index.js that
exports handleError (verified: exports.handleError = handleError).
- packages/api/package.json: name @librechat/api -> @hanzochat/api
- 254 source/config/doc/lockfile sites: @librechat/api -> @hanzochat/api
(api/, packages/, config/, turbo.json, pnpm-lock.yaml, bun.lock,
package-lock.json, LLM.md)
- @librechat/api-client (distinct MCP client id string) left untouched
- other @librechat/* packages (client, data-schemas, data-provider)
left as-is per scope
pnpm install (regenerated lockfile) + build:api green; dist exports
handleError. api/data-schemas/client packages build; full build:packages
and api test:ci are blocked by a PRE-EXISTING, unrelated upstream break
in librechat-data-provider (config.spec.ts / cloudfront-config.spec.ts
reference exports that src/config.ts no longer has) -- out of scope here.
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
helm values default of tag: latest violates the universe-wide semver-only
image tag policy. Pin to v1.0.0 and call out the policy in the comment so
the next override does not regress to latest.
* Add OBO (On-Behalf-Of) token exchange support for MCP server connections
Enables transparent authentication to Entra ID-backed MCP servers using the logged-in user's federated token via the OAuth 2.0 jwt-bearer grant. Configured via obo.scopes in librechat.yaml server config.
- Extract generic OboTokenService from GraphTokenService (jwt-bearer grant + cache)
- Refactor GraphTokenService to thin wrapper delegating to OboTokenService
- Add obo schema field to BaseOptionsSchema in data-provider
- Add resolveOboToken in packages/api/src/mcp/oauth/obo.ts (validates federated token, calls resolver, returns MCPOAuthTokens)
- Wire oboTokenResolver through MCPConnectionFactory, MCPManager, UserConnectionManager
- OBO tokens injected via request headers (not OAuth transport), refreshed on each tool call
- Explicit error on OBO failure (no fallthrough to standard OAuth redirect)
- Add unit tests for both resolveOboToken (9 tests) and exchangeOboToken (14 tests)
* Add OBO authentication option to MCP server UI configuration
Enable users to configure On-Behalf-Of (OBO) token exchange for MCP servers created via the UI (MongoDB-stored), in addition to the existing YAML-based configuration.
- Add "On-Behalf-Of (OBO)" radio option to MCP server auth section with scopes input field
- Remove obo from omitServerManagedFields so the field passes UI schema validation
- Add OBO to AuthTypeEnum, obo_scopes to AuthConfig, and OBO handling in form defaults and submission
- Add .min(1) validation on obo.scopes to reject empty strings
- Add English localization keys: com_ui_obo, com_ui_obo_scopes, com_ui_obo_scopes_description
- Add 5 schema validation tests for OBO field acceptance, transport compatibility, and edge cases
* 🧊 fix: Add obo to safe properties in redactServerSecrets. Fixes the OBO configuration not showing up in the MCP UI after app restart
* Address linter errors
* 🧊 fix: fail closed on OBO refresh errors and retry transient token exchange failures
- stop tool calls from falling back to stale Authorization headers when per-call OBO refresh fails
- add one-time retry for transient Entra OBO exchange failures (network/429/5xx)
- preserve structured OBO failure reasons and retryability in resolveOboToken
- improve OBO auth error messaging for connection setup and tool execution
- add tests for transient vs permanent OBO failure paths
* Addressing linting errors / warnings
* 🧊 fix: isolate OBO MCP auth to user-scoped connections
- block OBO-enabled servers from app-level shared MCP connections
- bypass shared connection lookup for OBO servers in MCPManager.getConnection
- add regressions covering OBO connection scoping and preserve non-OBO app connection reuse
* 🛠️ refactor: centralize MCP user-scoped connection policy
- add shared requiresUserScopedConnection helper for OAuth, OBO, and customUserVars
- use the shared predicate in MCPManager and ConnectionsRepository
- add utils coverage for user-scoped connection policy
* 🧊 fix: restrict MCP OBO config to header-capable transports
- Move OBO configuration out of the shared MCP base options schema and allow it
only on SSE and streamable-http transports, where request headers are applied.
- Explicitly reject OBO on stdio and websocket configs to avoid accepted-but-
nonfunctional server definitions. Add schema coverage for admin/config parsing
and user-input websocket validation.
* 🧊 fix: single-flight concurrent OBO token exchanges
Concurrent tool calls that arrive on a cache miss were each issuing
their own jwt-bearer request to the IdP. Under that fan-out, Entra
intermittently returned errors that the retry classifier saw as
non-retryable, surfacing as:
"The identity provider rejected the OBO token exchange.
Cannot execute tool <name>. Re-authenticate the user or
verify the configured OBO scopes and retry."
A user retry then hit the populated cache and succeeded, which matches
the observed flakiness — the cache was empty at the moment of fan-out
but populated by the time the user clicked retry.
- Coalesce concurrent exchanges in `OboTokenService.exchangeOboToken`
keyed by `${openidId}:${scopes}`. Callers that arrive while an exchange
is in flight share the same upstream request and receive the same
result. `fromCache=false` continues to force a fresh, independent
exchange (and is not joined by `fromCache=true` callers). The IdP
call, single-retry path, and cache write are unchanged — they were
moved into a `performOboExchange` helper so the coalescing wrapper
stays small.
- Tests cover: coalescing on the same key, isolation between different
keys, cleanup on success, cleanup on failure, and the
`fromCache=false` bypass.
* 🔒 feat: gate MCP OBO config behind MCP_SERVERS.CONFIGURE_OBO permission
OBO silently mints per-user delegated tokens from the caller's federated
access token and forwards them to whatever URL the server config points at.
Previously, anyone with MCP_SERVERS.CREATE could configure obo.scopes — so
if server creation is ever delegated beyond admins, a user could stand up
an attacker-controlled server, attach it to a shared agent, and exfiltrate
other users' downstream tokens on tool invocation.
Add a dedicated MCP_SERVERS.CONFIGURE_OBO permission (ADMIN: true, USER:
false by default) and enforce it at three layers so the safety property
no longer depends on CREATE staying admin-only:
- Create/update: POST/PATCH /api/mcp/servers returns 403 when the body
carries `obo` and the caller's role lacks the permission.
- Runtime fail-closed: for DB-sourced configs, MCPConnectionFactory and
MCPManager.callTool re-check the original author's role before each
OBO exchange. If the author has been downgraded, the exchange is
skipped (factory) or refused (callTool) — retained configs lose their
privileges automatically.
- UI: the OBO option is hidden in the MCP server dialog for users
without the permission; a CONFIGURE_OBO toggle is exposed in the MCP
admin role editor.
Existing role docs receive the new sub-key via the permission backfill
in updateInterfacePermissions on next startup, preserving any
operator-set values. YAML/Config-sourced server configs are unaffected
since they're admin-controlled at the deployment level.
* 🧊 fix: wire OBO machinery for servers with requiresOAuth: false
The discovery and user-connection paths gated OAuth wiring (flow
manager, token methods, oboTokenResolver, oboTrustChecker) behind
isOAuthServer(), which only considers requiresOAuth/oauth fields.
A DB-stored OBO server with requiresOAuth: false therefore landed in
the non-OAuth branch, never received an oboTokenResolver, and the
factory's usesObo getter evaluated to false — sending a bare request
that the upstream rejected with invalid_token.
Add requiresOAuthMachinery() (OAuth OR OBO) and use it at those two
gates. isOAuthServer remains for the OAuth-handshake-only check
(shouldInitiateOAuthBeforeConnect), where OBO must not initiate a
handshake. Plumb the OBO resolver/trust-checker through
ToolDiscoveryOptions so reinitMCPServer can pass them on the
discovery path.
* 🧊 fix: lock all OBO-target fields (URL, proxy, headers, auth) without CONFIGURE_OBO
The CONFIGURE_OBO permission was meant to gate control of the endpoint
that receives OBO-minted per-user delegated tokens and the scopes that
are requested. The previous frontend lock + backend gate only covered
obo.scopes and the auth section, leaving url/proxy/headers/etc. editable
by anyone with UPDATE — meaning a non-permission user could still
redirect an existing OBO server's token flow to an attacker endpoint.
Switch to an allowlist policy: when editing an OBO server without
CONFIGURE_OBO, only title/description/iconPath are mutable. Backend
rejects any other field change with 403; frontend disables the
non-allowlist sections (URL, transport, auth, trust) via fieldset.
The comparison surface (MCP_USER_INPUT_FIELDS) is derived from
MCPServerUserInputSchema's union members so it stays in sync with the
schema. New schema fields land in the locked set by default — adding to
the allowlist is the only way to unlock them, which preserves the
security-review boundary.
* 🧊 fix: skip unauthenticated MCP inspection for OBO-only servers
MCPServerInspector.inspectServer() ran an unauthenticated temp connection
unless the config had requiresOAuth or customUserVars set. For OBO-only
servers without standard MCP OAuth advertisement, this caused
MCPConnectionFactory.create to attempt the connection without a user or
oboTokenResolver — failing on servers that reject the MCP initialize
handshake without a valid bearer token, which surfaced as
MCP_INSPECTION_FAILED on create/update.
Add `obo` to the skip list alongside requiresOAuth and customUserVars,
matching the existing pattern for user-scoped auth modes.
* Addressed linting error: watchedTitle is declared but never referenced (the auto-fill logic at line 156 uses getValues('title') instead). Deleted constant.
* feat: use SecretInput for sensitive fields
* fix: align auth SecretInput styles
* chore: remove unused password i18n keys
* fix: align SecretInput controls
* fix: use SecretInput for dynamic credentials
* fix: reveal SecretInput controls on hover
* fix: align SecretInput eye icon and modernize controls
The wrapper was a flex container, so passing 'mb-2' on the input made it
contribute its margin to the wrapper's cross-axis size — the controls overlay
spanned the inflated height and centered the toggle 4px below the input's
true center. Switching the wrapper to a plain relative block collapses height
back to the input.
Also tightens the toggle/copy buttons (size-7 rounded-md with hover:bg-surface-hover)
and adds a focus ring on the input. Auth pages still override className/buttonClassName
so login/register styling is unchanged.
* fix: remove focus ring from SecretInput
* fix: keep green focus border on auth secret inputs
SecretInput's modernized default uses focus-visible:border-border-heavy and
hover:border-border-medium, which Tailwind emits after the auth pages' focus:
rules and overrides them. Auth pages now also declare focus-visible:border-green-500
and hover:border-border-light so cn()/twMerge resolves them as the winners
when classes are concatenated.
* feat: add optional sensitive flag to MCP customUserVars
Dynamic MCP credential fields all rendered as masked SecretInputs, which
also hid non-secret setup values like usernames, project keys, and URLs.
Add an optional `sensitive` flag to customUserVars and the plugin auth
config. It defaults to masked when omitted, so existing configs keep the
safe-by-default behavior; set `sensitive: false` to render a field as
plain text. The flag is display-only — values remain encrypted at rest.
Adds a coverage test that enumerates all registered models and asserts every
schema with a tenantId field has the tenant-isolation plugin applied (detected
via the global Symbol.for('librechat:tenantIsolation') marker the plugin sets),
with a single documented allowlist entry — SystemGrant, which scopes tenancy
manually. A future model that ships a tenantId field without the plugin (the
exact gap that lets data leak across tenants) now fails CI instead of shipping
silently.
Mongoose's distinct query operation was not in the tenant-isolation plugin's
hooked-operation list, so .distinct() (and .find(...).distinct(field), whose op
switches to 'distinct') ran unscoped — reading across all tenants. This affects
the ACL resource lookups (findAccessibleResources, findPublicResourceIds —
including PUBLIC 'shared-to-all' entries), agent category values, and random
prompt categories.
distinct IS a registerable query-middleware hook in Mongoose 8 (it is in
queryOperations), so the fix is to register the existing queryMiddleware for it
— one line. This keeps every call site as .distinct(), which is the established
FerretDB-compatible pattern (getRandomPromptGroups was deliberately built on
.distinct() rather than an aggregation stage for FerretDB support), and scopes
all distinct queries systemically with the same SYSTEM-context bypass and
strict-mode fail-closed behavior as the other operations.
Adds distinct cases to the plugin spec (including the find().distinct() op
switch and SYSTEM/no-context paths) plus behavioral tenant-isolation specs for
the ACL and category lookups; verified all fail without the hook.
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.
The ROLES cache is a single process-wide store, but role documents are
per-tenant (unique index { name, tenantId }). getRoleByName checked the cache
by role name BEFORE the tenant-scoped DB read, so a warm entry written under
one tenant's context was served to another tenant — leaking that tenant's
permission bits into the other's authorization decisions.
Scope every ROLES cache key with scopedCacheKey(), which appends the active
tenantId from the AsyncLocalStorage tenant context. It is a no-op when no
tenant context is set (or under runAsSystem), so single-tenant deployments
behave exactly as before.
Adds role.cache.spec.ts with a real Map-backed cache: two tenants sharing a
role name receive their own permissions, the cache key is tenant-scoped, the
same-tenant fast path still avoids a second DB read, and single-tenant mode
still uses the unscoped key.
* refactor(auth): migrate IAM auth to @hanzo/iam SDK
Replace manual OIDC implementation with @hanzo/iam/browser SDK:
- Add @hanzo/iam dependency for BrowserIamSdk
- Create utils/iam.ts with SDK singleton and config
- Update Login.tsx to use sdk.signinRedirect()
- Update OAuthCallback.tsx to use sdk.handleCallback()
- Update LandingPage.tsx to use SDK for login redirects
* ci: add E2E tests and PR preview workflows
- e2e.yml: Run Playwright tests on PRs touching client/api/e2e
- pr-preview.yml: Deploy PR preview via reusable DOKS workflow
Replace manual OIDC implementation with @hanzo/iam/browser SDK:
- Add @hanzo/iam dependency for BrowserIamSdk
- Create utils/iam.ts with SDK singleton and config
- Update Login.tsx to use sdk.signinRedirect()
- Update OAuthCallback.tsx to use sdk.handleCallback()
- Update LandingPage.tsx to use SDK for login redirects
* fix: Enforce MCP Permissions for Agent Tools
* fix: Measure MCP Image Limit by Decoded Size
* fix: gate cached MCP tools and tighten remote image URL detection
Addresses Codex review findings on the MCP permissions PR:
- filterAuthorizedTools previously fast-accepted any tool present in the
global tool cache before reaching the MCP-use permission gate. App-level
MCP tools (keyed `name_mcp_server` by MCPServerInspector and merged into
the cache via mergeAppTools) therefore bypassed the canUseMCP check,
letting a user without MCP_SERVERS.USE persist/bind them. Route all
MCP-delimited tools through the permission + server-access gate
regardless of cache presence.
- assertImageDataWithinLimit / image formatter used startsWith("http")
to skip the size cap, which also matched base64 payloads that happen to
begin with those chars. Require http:// or https:// via a shared
isRemoteImageUrl helper so oversized inline base64 can no longer bypass
MCP_IMAGE_DATA_MAX_BYTES.
Adds regression tests for both paths.
* fix: address Codex round-2 findings on MCP permissions PR
- parsers.ts: parseAsString dropped the image payload for unrecognized
providers, returning only `Image result: <mimeType>`. Pre-PR these
items survived via JSON.stringify(item). Keep the size guard but fall
through to JSON.stringify so the data/URL is preserved.
- MCP.js: the runtime MCP-use check only read `configurable.user`, so
paths that propagate `user_id` only (e.g. the OpenAI-compatible API in
agents/openai/service.ts) rejected every MCP tool call for an
authenticated user. Add resolveMCPPermissionUser: use the safe user
directly when it already carries a role (no extra DB call), otherwise
fall back to loading the role by user_id. Update fail-closed tests to
the resolved behavior.
- v1.js: the update path only re-filtered newly added MCP tools, so a
user who lost MCP_SERVERS.USE kept existing MCP bindings on edit while
create/duplicate/revert stripped them. Strip all MCP tools on update
when the permission is revoked; keep the narrower new-tool gating (and
disconnect/registry preservation) when it is intact.
Updates and adds regression tests for all three paths.
* fix: populate safe user at producer instead of resolving in runtime MCP check
Corrects the Finding B approach from the previous commit. Rather than
loading the user by id inside the runtime MCP permission check, populate
`configurable.user` (and createRun's `user`) with the full safe user at
the producer, matching the in-repo agent controllers
(responses.js / openai.js) which already pass `createSafeUser(req.user)`.
- service.ts: derive `safeUser` via createSafeUser(req.user) and pass it
to both createRun and processStream's configurable, so the role-bearing
identity reaches the runtime `userCanUseMCPServers(configurable.user)`
check. Falls back to a bare id when the host app attached no user,
which correctly leaves MCP gated (fail closed).
- MCP.js: revert the resolveMCPPermissionUser DB-load fallback; the
runtime check again reads configurable.user directly and fails closed
when absent (defense in depth).
- MCP.spec.js: revert to the matching runtime test expectations.
* test: cover safe-user propagation in createAgentChatCompletion
Adds a focused spec for the OpenAI-compatible chat completion service
(the producer fixed for Codex Finding B). Injects mocked deps and asserts
that createRun and processStream's configurable.user carry the role from
req.user (with sensitive fields stripped by createSafeUser), and that an
unauthenticated request falls back to a bare { id: 'api-user' } so the
runtime MCP check fails closed.
* fix: address Codex round-3 findings + TS6133
- MCP.js (P1): the assistants required-action path invokes tool._call(
toolInput) with no LangChain config, so the runtime check saw no
configurable.user and rejected authorized users. createToolInstance now
captures the creation-time user (req.user via createMCPTool) and _call
falls back to it for both the permission check and userId. Still fails
closed when neither config nor captured user carries a role.
- v1.js (P2): the update-path isMCPTool used a bare mcp_delimiter substring
check, misclassifying action tools whose operationId contains "_mcp_"
(e.g. sync_mcp_state_action_...) as MCP and dropping them on a
permission-revoked edit. Delegate to the canonical isActionTool so only
real MCP tools are gated. Regression test added.
- service.ts: drop the now-unused IUser import (TS6133); derive reqUser's
type from createSafeUser's own parameter instead.
* fix: resolve TS7022 self-reference in service.spec mock res
The mock response object referenced `res` inside its own `status`/`json`
initializers without a type annotation, so tsc inferred `res` as `any`
(TS7022). Annotate the object and assign the self-referencing chainable
methods after declaration.
* fix: correct round-4 findings (isActionTool import, captured user, partial-update)
- v1.js: import isActionTool from librechat-data-provider (its real export;
@librechat/api does not export it, so the prior import was undefined and
threw TypeError). Exclude action tools from MCP classification in both the
main filterAuthorizedTools loop and the update path, so action tools whose
operationId contains _mcp_ (e.g. sync_mcp_state_action_...) are preserved
regardless of MCP permission.
- v1.js: evaluate the effective tool set (updateData.tools ?? existingAgent.tools)
so a tools-less PATCH by a user who lost MCP_SERVERS.USE still strips stale
MCP bindings, matching create/duplicate/revert.
- MCP.js: createToolInstance now receives the construction-time user and _call
falls back to it (permissionUser) when configurable.user is absent, fixing the
assistants required-action path that invokes _call without a config and
resolving the capturedUser no-undef/ReferenceError.
- Tests: action-tool preservation (authorized + denied), tools-less revocation
PATCH, updated revocation test to expect all MCP tools stripped.
Affected specs pass locally: MCP 49/49, filterAuthorizedTools 49/49.
* fix: guard isActionTool against non-string tools; correct actionDelimiter import
Two test regressions from the prior commit:
- The main filterAuthorizedTools loop called isActionTool(tool) directly,
but isActionTool does toolName.indexOf(...) and throws on null/undefined.
Compute isActionToolName = typeof tool === 'string' && isActionTool(tool)
once and reuse it, restoring graceful null/undefined handling.
- The action-tool test referenced Constants.actionDelimiter (undefined);
actionDelimiter is a standalone librechat-data-provider export. Import and
use it directly.
filterAuthorizedTools 36/36 and MCP 40/40 pass locally.
* fix: address MCP permission review follow-ups
* fix: preserve shared agent MCP tools
* 🔒 fix: Strip post-login fields from unauthenticated /api/config response
Follow-up to #12490 reported in #12688.
The unauthenticated /api/config response still included fields that are
only consumed after login (helpAndFaqURL, sharedLinksEnabled,
publicSharedLinksEnabled, showBirthdayIcon, analyticsGtmId,
openidReuseTokens, allowAccountDeletion, customFooter, cloudFront).
None of these are read by the auth pages (Login, Registration,
RequestPasswordReset, ResetPassword, VerifyEmail, TwoFactorScreen,
AuthLayout, Footer, SocialLoginRender).
Split buildSharedPayload into two helpers:
- buildPreLoginPayload returns only the fields the unauthenticated auth
pages need (appTitle, server domain, social-login flags, OpenID/SAML
labels and image URLs, registration/email/password-reset flags,
minPasswordLength, ldap).
- buildPostLoginPayload returns the post-login informational fields and
is merged into the response only when req.user is present.
Also move buildCloudFrontStartupConfig into the authenticated branch:
useAppStartup is the only consumer and it runs after login.
Tests updated: existing CloudFront and allowAccountDeletion assertions
move to the authenticated context, and two new assertions cover the
stripped fields (one for the post-login informational fields, one for
cloudFront) in the unauthenticated context.
Signed-off-by: ChrisJr404 <chris@hacknow.com>
* fix: Request share-context startup config
* fix: Pass share startup config into footer
---------
Signed-off-by: ChrisJr404 <chris@hacknow.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
#12669 added `const { getAppConfig } = require('~/server/services/Config');` near the top of `config/set-balance.js` but the same import already existed lower in the file, producing:
config/set-balance.js
9:9 error 'getAppConfig' is already defined no-redeclare
The fork's sync CI surfaced this when its pre-commit hook ran eslint on the merged file. The upstream `eslint-ci.yml` is path-filtered on `api/**`, `client/**`, and `packages/**` — none of which match `config/**`, which is why CI didn't catch it upstream.
Drop the second declaration. Functionally identical, lint clean. No other changes.
- Update dependencies for @hyperdx/otel-web to 0.18.0 and @hyperdx/otel-web-session-recorder to 2.0.0
- Upgrade @hyperdx/instrumentation-exception to 0.3.0 and its dependencies
- Adjust peer dependencies and engine requirements for compatibility
* fix: honor admin-panel allowedDomains override at registration
registerUser called getAppConfig({ baseOnly: true }), which short-
circuits before any DB override merge. As a result, admin-panel edits to
registration.allowedDomains were silently ignored at signup, even though
they correctly apply to SSO callbacks via checkDomainAllowed (which
calls getAppConfig() with the full resolution).
The admin panel writes registration.allowedDomains to the __base__
principal in the configs collection. That principal is unconditionally
injected by getApplicableConfigs (no user identity required), so a
fully-resolved getAppConfig call picks up the override even before any
user exists. This aligns native signup with the SSO paths and lets
admins tighten or relax the allowed list without a backend restart.
Per review feedback: pass the ALS tenantId explicitly. /api/auth runs
through preAuthTenantMiddleware, which puts a tenantId into
AsyncLocalStorage. Mongoose queries inside getApplicableConfigs are
ALS-scoped, but the per-principal merged-config cache key uses the
*explicit* tenantId parameter (see overrideCacheKey in
packages/api/src/app/service.ts). If we leave tenantId undefined while
ALS holds tenant A, the merged result caches at `__default__` — and a
later request from tenant B would hit that entry, leaking tenant A's
allowedDomains (and balance) across tenants. Reading getTenantId() and
forwarding it makes the cache key match the DB scope, so __base__
overrides apply per-tenant correctly.
Behavior when no admin override exists is unchanged (the merged config
equals the YAML config; optional chaining handles missing fields).
Tests in AuthService.spec.js:
- Regression guard that getAppConfig is called with `{}` (no baseOnly)
when ALS has no tenant — protects against reintroduction of the
short-circuit.
- New tenant-context test verifying getAppConfig({ tenantId }) when
getTenantId() returns a tenant ID — protects against cross-tenant
cache bleed.
- Behavioral test confirming a disallowed domain returns 403 before any
DB user lookup.
* test: remove unused registerSchema import after merge resolution
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
The `set-balance` script called `getBalanceConfig()` without the app
config, so it always reported balance as disabled regardless of the
librechat.yaml configuration. Mirror the working `add-balance` script
by loading the app config first and passing it into `getBalanceConfig`.
Fixes#12413
Co-authored-by: Claude <noreply@anthropic.com>
* fix: 'Key ... not found in userinfo token!' for OPENID_REQUIRED_ROLE_TOKEN_KIND
Added userinfo as option to: OPENID_REQUIRED_ROLE_TOKEN_KIND handler.
Added a small refactor to case match the OPENID_REQUIRED_ROLE_TOKEN_KIND
setting and throw an explicit error.
* Addressed review feedback and switched from case match to if/else
* Extracted a function to be called so Admin and User token use same code to resolve token types
* feat(mcp/oauth): support audience parameter for Auth0/Cognito-style providers
LibreChat already follows RFC 9728 (Protected Resource Metadata discovery)
and RFC 8707 (resource indicators on /authorize). However, authorization
servers that pre-date RFC 8707 — most prominently Auth0 — issue
API-scoped access tokens only when an Auth0-specific 'audience' parameter
is supplied on /authorize and /token. Without it, refresh_token responses
strip the API audience and the next MCP call 401s.
This change adds an optional 'audience' field to OAuthOptionsSchema and
forwards it on:
* pre-configured authorize URL build
* discovered (DCR + RFC 9728) authorize URL build
* refresh_token grant body
'resource' (RFC 8707) is left untouched and remains the
standards-conformant route; 'audience' covers providers that ignore
'resource'. The two are independent — providers may accept either, both,
or neither, so we forward whichever the operator configures.
Schema tests added; no behavioral change for existing configs (field is
optional with no default).
Refs: MCP Authorization Spec 2025-06-18, RFC 9728, RFC 8707.
* ci: build audience-fix branch image to ghcr.io/freudator86/librechat:audience-fix
* Revert "ci: build audience-fix branch image to ghcr.io/freudator86/librechat:audience-fix"
This reverts commit 7b3dfa6cd7c67354137284cb600d0fd00f4735b9.
* tests: assert audience param in authorize URL + refresh body; tighten schema (.min(1)); refine comment to reflect actual code paths
Adresses PR review:
- audience: z.string().min(1).optional() rejects empty strings
- schema comment now precisely lists the two code paths (authorize + refresh_token grant); explicitly notes the authorization_code exchange intentionally does not receive audience because Auth0 binds it from the initial /authorize request
- new MCPOAuthAudience.test.ts: 4 cases — authorize URL with/without audience, refresh body with/without audience — using a local recording HTTP server (no shared helper changes)
- new schema test: empty-string audience is rejected
* style: inline two logger.debug calls (prettier)
* style: inline third audience-debug log (prettier)
* feat(mcp/oauth): add forward_audience_on_refresh opt-out for strict token endpoints (Cognito)
Addresses Codex review P2 'Avoid sending audience on refresh grants':
the previous behavior forwarded audience on every refresh_token grant,
which is correct for Auth0 (strips the audience claim otherwise) but is
non-standard for Cognito and other strict OAuth 2.0 token endpoints that
document refresh as grant_type + client_id + refresh_token only.
New optional boolean 'forward_audience_on_refresh' (default: true)
preserves the existing Auth0-friendly default while letting operators
of strict tenants opt out cleanly. Schema + handler tests cover both
cases.
No behavioral change for existing configs.
* style: format MCP OAuth refresh audience log
---------
Co-authored-by: Tim Freudenthal <tim@allesknut.de>
Co-authored-by: Danny Avila <danny@librechat.ai>
LibreChat sends `scope` on the refresh_token grant by default (PR #7924) because some authorization servers expect it. Salesforce rejects any scope on refresh with HTTP 400 "scope parameter not supported" (confirmed in production logs for case 00046259), which broke token refresh and forced re-authentication — amplifying the multi-replica PKCE retry storm.
RFC 6749 §6 makes scope optional on refresh (the server reuses the original grant). postRefreshRequest now sends scope as before and retries once WITHOUT it only when the failure is specifically a scope-parameter rejection (isScopeParameterRejection), so servers that need scope are unaffected and Salesforce-like servers self-heal with no operator config.
* ♻️ fix: Reap Stale In-Memory Generation Jobs to Prevent Heap OOM
InMemoryJobStore only reaped terminal jobs, so a generation that hung
without reaching completeJob() stayed "running" forever, retaining its
full message context. Abandoned jobs accumulated until the V8 heap was
exhausted (#13391). RedisJobStore already guards this with a 20-minute
running-job TTL; the in-memory store had no equivalent failsafe.
- Add a configurable staleJobTimeout (default 20m) to InMemoryJobStore;
cleanup() now reaps running jobs older than the timeout.
- Abort a pending generation in GenerationJobManager.cleanup() when its
job has been reaped, releasing client/graph references for GC.
- Abort the previous generation in createJob() when a job is replaced
for the same stream, closing an untracked-orphan leak.
- Forward staleJobTimeout through createStreamServices.
* 🩹 fix: Remove same-stream replacement abort (codex P1)
The createJob replacement-abort could let a stale, replaced request take
the abort-during-initialization path and complete/error the replacement
job via the shared streamId, and was a no-op across Redis replicas.
Removed it; the reported OOM is handled by the running-job failsafe and
the orphan-loop abort, which only fires when no job holds the streamId.
* 🩹 fix: Reap stale jobs on inactivity rather than age (codex P2)
Age-based reaping would drop a legitimately long but actively-streaming
generation at the timeout. Track a last-activity timestamp (refreshed on
each emitted chunk via recordActivity) and reap on inactivity instead,
mirroring RedisJobStore refreshing the running TTL on each appendChunk.
* 🩹 fix: Notify reaped streams and reset activity on replacement (codex P2)
- Emit a terminal error to any client still attached when a stale job is
reaped, so the SSE connection closes instead of hanging open with no
final/error event.
- Clear lastActivity in createJob so a replacement reusing the same
streamId falls back to its fresh createdAt and isn't reaped immediately
on the previous generation's stale activity timestamp.
* 📤 feat: Model-Aware Max Output Tokens for Google/Gemini
Resolves#13384.
Current Gemini text models (2.5 and 3+, including Gemini 3.5 Flash)
support 64K output tokens, but LibreChat defaulted every Google model
to the legacy 8K value — most visibly in the Agents model-parameter
panel.
- Add model-aware `reset`/`set` to `googleSettings.maxOutputTokens`,
mirroring the Anthropic pattern: Gemini 2.5/3+ -> 65536, legacy
(2.0 and earlier) and Gemma -> 8192.
- Resolve the default server-side in `getGoogleConfig` and in the
Agents, preset, and standard Google settings panels via a shared
`applyModelAwareDefaults` helper.
- Make `compactGoogleSchema` and `generateGoogleSchema` model-aware so
explicit user values are preserved and not overwritten.
* 🛡️ fix: Cap Google max output at Vertex-safe limits
Addresses Codex review (P1) on #13390. Vertex AI caps current Gemini
text models at 65,535 output tokens (vs 65,536 on AI Studio) and image
models at 32,768, so an unconditional 65,536 default could make
otherwise-default Vertex requests fail validation.
- Lower the modern text default/ceiling to 65535 (valid on both Vertex
and AI Studio).
- Resolve Gemini image models (e.g. gemini-2.5-flash-image) to 32768.
- Add reset/set + getGoogleConfig tests for image models and the Vertex
default path.
* 🧮 fix: Respect configured Google defaults and legacy image caps
Addresses Codex review round 2 on #13390 (one P2, two P3).
- P2 (llm.ts): apply the model-aware maxOutputTokens default as the final
fallback instead of pre-filling it, so an explicit value, `defaultParams`,
and `addParams` all take precedence and `dropParams` is honored. Empty-string
values stay stripped (preserves prior Gemini empty-payload handling).
- P3 (panels): pass the resolved params endpoint (`overriddenEndpointKey`) to
`applyModelAwareDefaults`, so custom endpoints with
`defaultParamsEndpoint: 'google'` also surface the model-aware default.
- P3 (schemas): nest the image-model check inside the 2.5+/3+ version check, so
legacy image IDs (e.g. gemini-2.0-flash-preview-image-generation) keep the 8K
cap instead of being treated as 32K models.
- Add tests for defaultParams precedence, dropParams, legacy image models, and
the Vertex default path.
* 🧭 fix: Base Google defaults on final model and configured overrides
Addresses Codex review round 3 on #13390 (two P2).
- llm.ts: resolve the model-aware maxOutputTokens default from the final
`llmConfig.model` (after defaultParams/addParams) instead of the model
captured from modelOptions, so a model forced via addParams/paramDefinitions
on a Google-compatible custom endpoint gets its correct limit.
- Panels: apply model-aware defaults to the built-in settings first, then
overlay `customParams.paramDefinitions`, so an admin-configured
maxOutputTokens default wins in the UI (consistent with backend precedence).
- Add parameterSettings.spec for applyModelAwareDefaults (incl. override
precedence) and a getGoogleConfig final-model test.
* 🧠 fix: Replay DeepSeek `reasoning_content` via OpenRouter
DeepSeek's thinking-mode API rejects multi-turn tool-calling requests
unless `reasoning_content` from each tool-bearing assistant message is
replayed verbatim, returning HTTP 400 "The `reasoning_content` in the
thinking mode must be passed back to the API." The agents SDK already
handles this for direct `Providers.DEEPSEEK`, but DeepSeek models routed
via OpenRouter use `Providers.OPENROUTER` — `formatAgentMessages` skipped
the reasoning-preservation branch, and `ChatOpenRouter` left
`includeReasoningContent` unset, so the field silently dropped on every
subsequent turn.
Add `isDeepSeekReasoningProvider(provider, model)` and use it in two
places: (1) `getOpenAILLMConfig` flips `includeReasoningContent: true`
when OpenRouter is dispatching a `deepseek/*` model so the LangChain
client emits the field on assistant turns that have non-empty
`additional_kwargs.reasoning_content`, and (2) `AgentClient` spoofs the
provider hint to `Providers.DEEPSEEK` when calling
`formatAgentMessages`, triggering the SDK's existing
`preserveReasoningContent` path that re-attaches the field to
reconstructed tool-bearing AIMessages. The downstream
`_convertMessagesToOpenAIParams` is already gated on non-empty
`reasoning_content`, so the flag is a no-op outside thinking mode.
Resolves#13366.
* fix: Harden DeepSeek detection against OpenRouter routing edges
Address three Codex review findings on #13368:
1. Strip OpenRouter's `~` latest-routing prefix before applying the
DeepSeek model regex. `~deepseek-chat` and `~deepseek/r1` were
previously left unmatched because the regex's start/`/` boundary
only saw the `~`. Mirror the SDK's `normalizeOpenRouterModel()`
here and in `getOpenAILLMConfig`.
2. Add a custom-endpoint fallback: when the model id carries the
unambiguous `deepseek/...` OpenRouter namespace, accept it
regardless of the resolved provider. Covers the case where a user
configures OpenRouter under a non-standard endpoint name and
`initializeAgent` normalizes the unknown provider to `openai`,
stranding the spoof. Bare `deepseek-*` ids still require an
explicit DeepSeek/OpenRouter provider so unrelated endpoints
labelling a model `deepseek-r1` don't trigger.
3. Inspect every agent in `this.agentConfigs` when deciding whether
to spoof the format provider. Multi-agent handoff runs feed all
agents' messages through one `formatAgentMessages` call, so a
DeepSeek handoff under a non-DeepSeek primary previously lost its
persisted reasoning_content too.
Also addresses Copilot's review note: only pass the options object
to `formatAgentMessages` when the DeepSeek spoof is actually needed,
preserving the pre-fix behavior for everyone else.
* fix: Extend DeepSeek reasoning_content fix to OpenAI-compat agent paths
Address two more Codex P2 findings on #13368:
1. `getOpenAILLMConfig` no longer gates `includeReasoningContent` on
`useOpenRouter`. Any DeepSeek-style model id (with `~` latest-routing
prefix stripped) is sufficient. This re-aligns the LLM gate with
`AgentClient`'s formatter spoof, which already treats a `deepseek/*`
id as authoritative — so a custom-named OpenRouter endpoint or a
DeepSeek-compatible proxy gets the field both attached to history AND
serialized to the wire. Direct `ChatDeepSeek` ignores the flag (its
own conversion path hardcodes `includeReasoningContent: true`), so
this is a harmless no-op there.
2. Thread the same `Providers.DEEPSEEK` formatter hint through
`api/server/controllers/agents/openai.js` and `responses.js` (the
OpenAI-/Responses-compatible serving paths). Without it those paths
restored `additional_kwargs.reasoning_content` only in `AgentClient`
while the LLM config flipped `includeReasoningContent` on for them
too — so DeepSeek tool turns served from those endpoints would still
ship requests with the flag set but no field present, hitting the
same second-turn 400. The `needsDeepSeekFormatHint` helper in
`openai.js` mirrors `AgentClient`'s per-agent check.
* fix: Tighten DeepSeek detection and cover handoff sub-agents
Address four more Codex P2 findings on #13368:
- Tighten the DeepSeek model regex to `^deepseek(?:[-/]|$)/i` (anchored
to start). Rejects cloned/distilled slugs like
`mistral/deepseek-distilled-foo` and `community/deepseek-r1` that
previously matched via the `(?:^|/)` alternation, which could attach
the DeepSeek-only `reasoning_content` field on proxies that don't
accept it.
- Anchoring also collapses the namespace-only fallback into the same
pattern, so bare `deepseek-chat` / `deepseek-reasoner` on a
custom OpenAI-compatible DeepSeek proxy are now recognized — fixing
the asymmetry where `getOpenAILLMConfig` would flip
`includeReasoningContent` for those bare ids but `AgentClient`
wouldn't pass the formatter hint.
- Extend `needsDeepSeekFormatHint` in `openai.js` (and the inline
check in `responses.js`) to walk `handoffAgentConfigs` too. In
multi-agent runs where the primary isn't DeepSeek but a connected
handoff agent is, the SDK's `formatAgentMessages` previously dropped
the handoff's persisted reasoning_content before the next tool turn,
preserving the 400 the PR was meant to prevent.
- Mirror the regex change in `getOpenAILLMConfig`.
Out of scope: the OpenAI-compatible serving paths still don't
preserve incoming `reasoning_content`/`reasoning` fields in
`convertMessages`, nor does the Responses API persist reasoning in
`saveResponseOutput`. Those are deeper persistence/conversion fixes
worth a separate PR.
* test: Allow includeReasoningContent for Azure-serverless DeepSeek
CI surfaced a backward-compat expectation that snapshotted the
pre-fix behavior. Azure-serverless DeepSeek deployments (e.g.
`DeepSeek-R1`) forward to the same DeepSeek thinking-mode tool-call
contract, so the LLM gate now correctly flips
`includeReasoningContent: true` for them too. The downstream
gate on a non-empty `additional_kwargs.reasoning_content` keeps
this a no-op outside thinking mode.
* chore: Trim noisy comments
Per CLAUDE.md ("self-documenting code; no inline comments narrating
what code does"), strip the multi-paragraph rationale that crept into
the DeepSeek reasoning_content fix. The commit history and PR
description carry the why; the code says the what.
Keeps one single-line JSDoc on `isDeepSeekReasoningProvider` (linking
to the DeepSeek docs) and a `(#13366)` tag on each opt-in site so
future readers can find the context.
* revert: Drop non-functional DeepSeek hint from OpenAI-compat serving paths
Codex's later review passes correctly flagged that threading the
DeepSeek formatter hint through openai.js (`/v1/chat/completions`) and
responses.js (`/v1/responses`) doesn't actually fix the second-turn
400 in those paths. Empirical check against the real SDK confirmed the
gap is deeper and pre-existing:
formatAgentMessages(payload, ..., { provider: DEEPSEEK })
where payload is the `convertMessages`/`convertInputToMessages` output
shape (string content + TOP-LEVEL `tool_calls`) produces NO tool-bearing
AIMessage at all — `formatAssistantMessage` only reconstructs tool calls
from `tool_call`-typed *content parts*, never a top-level `tool_calls`
field. So those serving paths don't reconstruct tool-call history (let
alone reasoning) regardless of the hint. The Responses persistence layer
likewise stores only output text, not tool calls or reasoning.
Making those paths work requires reworking the wire->internal message
conversion (and Responses persistence) to emit content-part arrays — a
broad, pre-existing concern beyond this issue and risky to land here.
Rather than ship a hint that looks like a fix but is inert, revert the
serving-path changes and scope this PR to the validated AgentClient
chat path (the actual surface in #13366).
Reverts the openai.js/responses.js threading and their spec mocks to
main. Keeps the AgentClient fix, `isDeepSeekReasoningProvider`, the
`getOpenAILLMConfig` flag, and the type.
* 🛡️ fix: Cap Default Limit on Agent List Queries (#13363)
`GET /api/agents` accepted unbounded requests: when the client omitted
`limit`, the value flowed straight into `getListAgentsByAccess`, which
set `isPaginated = false` and issued an uncapped MongoDB query. Combined
with the unindexed `findPubliclyAccessibleResources` AclEntry scan run
on every request, this produced 10-19s response times and stalled the
connection pool on instances with 100+ agents.
- Default `limit` to 100 in the route handler so client requests without
`?limit=` paginate by default.
- Default `limit` to 100 in `getListAgentsByAccess` itself as
defense-in-depth. The function already caps numeric limits at 100, so
there is no client-facing change.
- Pass `limit: null` explicitly in the actions route, which legitimately
needs the full editable-agent set, to preserve its existing behavior.
- Add regression tests covering the default cap and the explicit
unbounded opt-out.
* 🛡️ fix: Avoid agent-list regression for users with 100+ agents
Codex review pointed out that capping `getListAgentsByAccess` at 100
silently truncated agents past the first page for the four consumers
(`useAgentsMap`, `AgentSelect`, `ModelSelectorContext`, `useMentions`)
that read `res.data` without following `has_more`/`after`.
- Raise the function's hard cap from 100 to 1000 to match
`MAX_AVATAR_REFRESH_AGENTS`, the realistic upper bound the
avatar-refresh path already assumes. (Side effect: the avatar refresh
call site was silently being capped at 100 by the old normalize step.)
- In `useListAgentsQuery`, merge `limit: 1000` into params so the four
consumers above get the user's full accessible set in a single
round-trip instead of needing cursor pagination.
- Route handler default stays at 100 as defense-in-depth for any other
caller that omits `limit`.
- Add a regression test asserting an explicit `limit` above 100 now
returns the full set instead of being clipped.
* 🪢 fix: Keep agent-list cache key stable for mutations
Codex P2 review noted that folding `limit: 1000` into the cache key
broke `allAgentViewAndEditQueryKeys` in `Agents/mutations.ts`, which
references `[QueryKeys.agents, { requiredPermission }]` directly across
eight mutation handlers. After my prior change the cached entry lived
under `[QueryKeys.agents, { limit: 1000, requiredPermission }]`, so
create/update/delete/avatar/action mutations stopped updating the list
the four consumer hooks render — and with `refetchOnMount` and focus/
reconnect refetches disabled, the UI would stay stale until something
else triggered a fetch.
Split the merged limit out of the cache key: the request to
`dataService.listAgents` still uses `requestParams` (with the default
limit applied), but the React Query cache key uses the caller's `params`
as-is. The mutation cache updates land again, and the request still
returns the user's full accessible set in one round-trip.
* 🛡️ fix: Index AclEntry and paginate agent list internally (#13363)
Completes the perf fix for #13363 properly — resolves both the
unbounded ACL scans Copilot flagged and Codex's tension between "show
all agents" and "don't bypass the server cap".
Backend:
- Add a compound index on `{ principalType, resourceType, permBits,
resourceId }` to the AclEntry schema. This is the index missing for
`findPublicResourceIds` and the public branch of the `$or` in
`findAccessibleResources`, both of which previously fell back to a
collection scan on every `GET /api/agents`. Adds an `explain`-based
regression test asserting the public query no longer COLLSCANs.
Client:
- Rewrite `useListAgentsQuery` to follow the server's cursor
pagination internally and concatenate every page into a single flat
`AgentListResponse`. Consumers (`useAgentsMap`, `AgentSelect`,
`ModelSelectorContext`, `useMentions`) get the user's complete
accessible-agent set without any of them needing to learn about
cursors, and each individual request uses the server's default
page size (so the route's 100-default defense-in-depth fires for
real). Cache key shape is unchanged, so the eight mutation handlers
in `Agents/mutations.ts` keep matching `allAgentViewAndEditQueryKeys`
and update the cached list as before.
- Drop the `FULL_AGENT_LIST_LIMIT = 1000` injection added in the
previous commit — no longer needed once pagination handles the full
set, and removing it stops bypassing the route default.
* 🧹 fix: CI fallout from C-done-properly refactor
- Collapse multi-line `fetchAllAgentPages` signature in queries.ts so
prettier stops complaining.
- In the new public-principal index test, grant one ACL entry before
calling `.explain()` so the collection exists (otherwise mongo returns
`nonExistentNamespace` and there is no winning plan to inspect).
- Cast the `.explain('queryPlanner')` result to a typed shape — the
mongoose return type doesn't expose `queryPlanner` directly and was
failing the TypeScript check.
* 🧪 fix: Test the AclEntry public-principal index via hint, not planner choice
The previous test asserted the query planner did not pick COLLSCAN for
the public-principal lookup. That assertion fails on small collections
(under the planner's collection-size heuristic) — the index exists and
is usable, but with a single document in the test the planner correctly
chooses COLLSCAN as the cheaper plan.
Reshape the assertion:
1. Confirm the new compound index is actually declared by inspecting
`collection.indexes()` after `syncIndexes()`.
2. Force the planner to that index via `.hint()` and assert the winning
plan is `IXSCAN` — proves the index is real and serves this query
shape, without depending on collection-size heuristics.
* 🧹 chore: Slim down verbose comments
The JSDoc and inline comments added across the perf fix had drifted
into multi-paragraph rationale better suited to the PR description than
the source. Collapse to single-line JSDoc that just describes what each
piece does; drop the inline comment in `actions.js` entirely — the call
is self-evident.
* feat: add Claude Opus 4.8 support
* fix: omit sampling params for Claude Opus 4.8
* fix: flatten Bedrock beta header merge
* fix: strip Bedrock sampling params for Opus 4.8
The `Tests: @librechat/api` job in `backend-review.yml` had
`timeout-minutes: 10`. Recent runs have started getting cancelled
right at the 10-minute mark with tests still actively passing — the
log streams `PASS …` lines through the final second before
`The operation was canceled.` fires.
Looking at recent runs in the wild:
- Warm `ubuntu-latest` runners: suite finishes in ~4–5 min.
- Cold/busy runners: 10+ min. No headroom = job killed mid-suite.
The suite has grown over the last few months (OpenTelemetry, MCP
OAuth, Bedrock, the Operational Prometheus Metrics work from #13265,
plus the agents/responses harness). 10-minute ceiling that worked
before is now a tail-latency cliff.
Bump to 20 minutes. Still well below the runner's hard 6-hour cap,
generous enough to absorb runner variance, and a stuck/genuine hang
will still get killed eventually rather than billing forever.
No code changes; one-line config bump.
In SidePanel/MCPBuilder/MCPServerCard the server title was rendered in
a span carrying the Tailwind truncate utility. overflow: hidden does
not apply to inline boxes, so long server names overflowed their slot
and ran underneath the Edit, Reconnect, and Revoke icons.
The title now renders in a div so truncate actually clips with an
ellipsis. No other styling changes.
* chore: Update @librechat/agents to version 3.1.93 and @langfuse packages to version 5.3.0 in package-lock.json and package.json files
* chore: Update browserify-sign to version 4.2.6 and qs to version 6.15.2 in package-lock.json
* fix(redis): add REDIS_CLUSTER_SAFE_DELETE for ElastiCache Serverless CROSSSLOT errors
ElastiCache Serverless and similar managed Redis services present a single-node
connection endpoint but shard keys internally. When USE_REDIS_CLUSTER=false (as
required for single-endpoint services), batchDeleteKeys() uses multi-key DEL
commands that fail with CROSSSLOT errors because the managed cluster rejects
cross-slot operations.
Adds REDIS_CLUSTER_SAFE_DELETE=true which forces per-key deletion (the same
cluster-safe path) without changing the connection mode. This makes the delete
strategy independent of the connection topology.
Closes#13261
* test(cache): add REDIS_CLUSTER_SAFE_DELETE config tests
* fix: Avoid nested Redis delete mode ternary
* docs: Add Redis cluster-safe delete env example
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
Adds a `Run Prettier --check on changed files` step to the existing
`eslint-ci.yml` workflow. Same path filter (api/**, client/**,
packages/**), same changed-files detection, runs after the ESLint step.
## Why
Today there is no `prettier --check` in CI — only the local
`lint-staged` pre-commit hook runs `prettier --write`. When a PR is
merged with the hook bypassed (e.g. GitHub UI edit-and-merge, or
`git commit --no-verify`), a file can land in a non-prettier-canonical
state and nobody notices. The next contributor who stages an unrelated
change in that file then ends up with a "drive-by" prettier diff in
their PR.
`packages/api/src` had 14 such files accumulated; #13281 fixes the
existing drift. This PR closes the gap so it doesn't regrow.
## What the step does
- Detects changed JS/TS files under `api/**`, `client/**`, or
`packages/**` against the PR base.
- Runs `npx prettier --check $CHANGED_FILES`.
- On failure, prints a `::error::` annotation telling the contributor
how to fix it locally (`npx prettier --write <files>`).
Same one-step shape as the existing ESLint check — no extra workflow
file, no extra `npm ci`, no extra checkout.
## Ordering note
#13281 (`chore: prettier --write packages/api/src`) should land first
so the existing drift is cleared. After both PRs merge, the
pre-commit hook + this CI check together prevent drift from
re-accumulating.
## Test plan
- [x] `npx js-yaml .github/workflows/eslint-ci.yml` validates.
- [ ] CI green on this PR itself (touches only `.github/workflows/`,
which the path filter includes, so the workflow runs on itself).
- [ ] After merge: a synthetic PR introducing prettier drift should
fail the new step with the diagnostic message.
Run `prettier --write` over the source trees of every workspace to align
with the repo's own `.prettierrc` (`printWidth: 100`, `singleQuote: true`,
`trailingComma: 'all'`, etc.). **19 files reformatted total** — purely
whitespace and line-wrap changes, no functional edits and no API changes.
Scope:
- `packages/api/src/**/*.{ts,tsx}` — 14 files
- `packages/client/src/**/*.{ts,tsx}` — 1 file
- `packages/data-schemas/src/**/*.{ts,tsx}` — 4 files
- `api/**`, `client/**`, `packages/data-provider/**` — already prettier-clean
Most of the drift is in argument-list / type-annotation wrapping where
the formatted form fits within `printWidth` but the current source keeps
a hand-wrapped multi-line shape. Example:
// before
function countWebSearchDefinitions(
toolDefinitions: Array<{ name: string }> | undefined,
): number { … }
// after (still well under 100 cols)
function countWebSearchDefinitions(toolDefinitions: Array<{ name: string }> | undefined): number { … }
`npx prettier --check` across all workspaces is now clean. The local
pre-commit hook (`lint-staged` → `prettier --write`) would have produced
the same result on any future edit to these files.
There are no prettier-checking workflows in CI today, so drift like this
can re-appear if PRs are merged with the hook bypassed. Companion PR
#13282 adds a `prettier --check` step to `eslint-ci.yml` so future
drift gets caught.
* 🪟 fix: Apply Admin-Panel Config Overrides To YAML-Defined MCP Servers
Admin-panel saves of MCP server fields for YAML-defined servers were
silently dropped by the registry. ensureConfigServers filtered out any
merged config entry whose name appeared in YAML, so overrides such as
iconPath, title, and description never reached getAllServerConfigs even
though the override row had been written to the configs collection and
the AppConfig merge layer had produced the correct merged result.
The filter is removed and replaced with a content-equivalence
short-circuit in ensureSingleConfigServer. YAML-defined servers whose
merged config matches the YAML cache entry skip lazy-init, so unmodified
YAML servers still avoid a redundant inspection round trip. The new
private helper matchesYamlConfig reuses the existing content-hash
function on configurable fields only.
getAllServerConfigs now overlays config-tier entries onto the YAML base
while preserving user-DB entries (source: 'user'), giving precedence of
YAML, then Config tier, then User DB. The docstring is updated to
describe the new order.
Multi-tenancy is already enforced upstream of the registry by the
AppConfig layer, so the registry stays tenant-agnostic and overrides
remain isolated per tenant.
Tests cover the new behavior: config-tier override on YAML-defined
server flows through to getAllServerConfigs, YAML servers without
effective overrides skip lazy-init, user-DB entries win over
config-tier overlays, pure config-tier servers still lazy-init, and the
merged config passed to lazy-init preserves all YAML fields when the
override only adds new ones.
* 🛠️ fix: Address Review Feedback For YAML Override Precedence
Both Copilot and Codex flagged matchesYamlConfig as broken in
production: the cached YAML config carries inspector-derived defaults
(requiresOAuth defaulted to false when YAML omits it, serverInstructions
rewritten from the YAML toggle to the fetched server-instructions
string, and so on) that are absent from appConfig.mcpConfig. The
content-hash comparison reports a mismatch for every YAML server with
no admin-panel override, so ensureSingleConfigServer re-inspects all of
them anyway. The optimization never fires in practice and reintroduces
the source tagging it was supposed to avoid.
Remove the short-circuit and the matchesYamlConfig helper. YAML-defined
servers that appear in the merged config go through lazy-init like any
other entry. A smarter optimization that overlays cosmetic-only
override fields onto the YAML cache without re-inspection belongs in a
follow-up once the boundary between configurable fields and
inspector-derived fields is well defined.
Update the existing ensureConfigServers tests that asserted the old
filter behavior (should exclude YAML servers from config-source
detection, should return empty when all servers are YAML) to assert
the new behavior: YAML servers pass through ensureConfigServers and
are lazy-initialized when they appear in the merged config. Make the
inspector mock more realistic by spreading the raw input first and
overlaying only runtime fields, so the test fixtures match production
where the inspector preserves configurable fields. Drop the companion
test in MCPServersRegistry.test.ts that asserted the short-circuit
fires for unchanged YAML servers; the hand-crafted fixture skipped
inspector defaults and was not representative.
Copilot also flagged that getServerConfig short-circuits to
configServers before checking the user DB, so the precedence enforced
in getAllServerConfigs (user-DB beats config-tier) was bypassed in the
single-server lookup. When configServers carries an entry and a userId
is available, check the user DB first and prefer a source: 'user'
entry so per-user servers are never shadowed by an admin-panel
override.
* 🛡️ fix: Harden Admin Override Overlay Against Failure Stubs
Hardens the admin-panel override path against transient inspect failures
and removes a defensive branch that guarded an impossible state.
The getAllServerConfigs overlay now skips failed-inspection stubs so a
healthy YAML or DB entry stays visible during the 5-minute retry window
instead of being clobbered by a stub. When the overlay does land, the
base entry's source tier is preserved, which keeps Tools/mcp.js routing
its failed-inspection recovery to the correct storage location.
The user-DB precedence block in getServerConfig is removed: configServers
is built from appConfig.mcpConfig which only ever carries admin-tier
entries, so the DB lookup defended a state that cannot occur via the
current call graph. The dead yamlServerNames memoization is also gone.
Adds two regression tests covering inspection-failure preservation and
source-tier preservation on successful overlay, and adds a debug log
when an admin override is suppressed by a user-tier entry. The
makeParsedConfig test factory now honors overrides correctly.
* 🧪 test: Strengthen Admin Override Coverage And Docs
Adds an end-to-end regression test that chains MCPServerInspector.inspect
failure through ensureConfigServers and getAllServerConfigs, asserting
the healthy YAML base entry survives a transient inspect failure
intact. The previous regression test hand-built a failure stub and
skipped ensureSingleConfigServer, leaving the production chain itself
untested.
The getAllServerConfigs docstring now spells out both overlay guards
(failed-stub skip and user-tier preservation) and the source-field
preservation contract that downstream recovery logic depends on.
The yamlLangfuseConfig test fixture is frozen so a future test cannot
mutate it and contaminate sibling tests in the describe block.
* 🔧 fix: Skip Lazy-Init For Unchanged YAML MCP Servers
Adds an admin-configurable-field equivalence check so YAML-defined MCP
servers that carry no admin override skip lazy-init in
ensureConfigServers. This avoids the per-request inspect storm and
keeps unmodified YAML servers out of the config-tier cache, so admin
saves that touch unrelated overrides no longer evict and tear down
those YAML connections.
A second guard in getServerConfig prevents failed-inspection stubs in
configServers from shadowing the healthy YAML base entry for the
duration of the retry window. The aggregate path already had this
guard via getAllServerConfigs; this brings the single-server path to
parity, so Tools/mcp.js recovery routes to YAML reinspection rather
than bailing on the config retry timer.
Adds three regression tests covering the unmodified-YAML skip, the
admin-override lazy-init trigger, and the failure-stub fallthrough.
Updates two existing ensureConfigServers tests that previously
documented the now-incorrect "always lazy-init YAML" behavior.
* 🔓 feat: Expose baseOnly Flag On Admin Config Base Endpoint
The admin getBaseConfig handler now reads req.query.baseOnly and
forwards it to getAppConfig so an admin panel client can request the
un-merged YAML and AppService base configuration without DB overrides
applied. The flag is opt-in; existing callers see no behaviour change
because the default remains the merged response.
The query value is coerced through String() so Express array forms
like baseOnly=true&baseOnly=true are treated as false rather than
truthy by accident. A handler test pins the forwarding behaviour and
the default-merged behaviour against future regressions.
* 🧹 fix: Address Codex Review Findings On MCP Registry Precedence Path
Four follow-ups from the Codex review of PR #13173:
P1. getServerConfig now preserves the configServers candidate as a
last-resort fallback when both YAML cache and user DB return nothing,
so admin-defined config-only servers carrying inspectionFailed=true
still surface the failure stub to callers in api/server/services/Tools/mcp.js
that rely on it to return the still-unreachable message. The
not-found memoization is preserved.
P2a. proxy is added to ADMIN_CONFIGURABLE_FIELDS so an admin override
on SSE/streamable-http proxy is no longer treated as an unchanged YAML
server and correctly triggers lazy-init.
P2b. isUnmodifiedYamlServer now treats absent-on-rawConfig fields as
equal, so inspector-derived values on the cached YAML entry
(notably requiresOAuth filled in by detectOAuth at startup) do not
force unmodified YAML servers to re-init on every request.
P3. getBaseConfig parses ?baseOnly strictly against the literal string
true instead of String-coercing, so array shapes like baseOnly[]=true
no longer pass through.
Regression tests cover all four paths.
* 🧹 fix: Drop Misleading Shadow Warning On Config Vs User-DB Collisions
The Config-tier branch of warnOnOperatorManagedNameCollisions logged
that Config MCP servers shadow DB-backed servers, but
getAllServerConfigs actually preserves the user-tier entry on a
Config-vs-user collision and skips the override. The warning was
describing the opposite of what the code does and would mislead
operational debugging.
The YAML-tier call is unchanged because YAML still legitimately
shadows DB-backed servers. The per-entry debug log inside the
collision branch already captures the actual outcome.
Test renamed and rewritten to assert the user-tier entry is
preserved and no shadow warning is emitted.
* 🔒 fix: Keep Tenant-Scoped configServers Candidate Out Of The Global Read-Through Cache
The prior fix for surfacing inspectionFailed stubs from admin-defined
config-only servers wrote the per-call configServers candidate into
readThroughCache when YAML and DB both missed. The cache key is keyed
by serverName plus userId, so a failed stub from one tenant could
satisfy a later no-userId lookup made by another tenant before any
configServers resolution ran.
getServerConfig now caches only the global YAML/DB resolution (still
caching undefined to memoize not-found lookups) and uses the candidate
strictly as an unmemoized function-level fallback that surfaces the
failure stub to the caller without leaking it across tenants.
Regression test exercises a no-userId call after a tenant-scoped
failure and asserts the cache returns undefined rather than the
stub, and that a second tenant sees their own healthy candidate.
* 🔄 fix: Mirror getAllServerConfigs Precedence Exactly In getServerConfig
getServerConfig was short-circuiting with the configServers candidate
on every healthy lookup, which made single-server callers diverge from
the aggregate path for name collisions between config-tier overrides
and user-DB entries. The aggregate path preserves the user-tier entry
on such collisions, so single-server callers saw the admin override
while list views saw the user server for the same name.
getServerConfig now resolves the YAML/DB base first and applies the
same four-step precedence used in getAllServerConfigs:
1. user-tier base wins absolutely over a config-tier candidate
2. healthy YAML/DB base wins over a failed (inspectionFailed)
candidate
3. healthy candidate overlays its fields onto the base, preserving
the base entry's source tag so downstream recovery routes to the
correct storage location
4. with no base, the candidate is returned as-is for config-only
servers
readThroughCache still memoizes only the global YAML/DB lookup, so
the per-call configServers candidate never enters the cache and the
tenant-isolation guarantee from the previous fix is preserved.
Regression tests cover the user-wins-over-config case and the
YAML-overlay-with-yaml-source-preserved case.
* ⚡ perf: Batch YAML Cache Read In ensureConfigServers
isUnmodifiedYamlServer was calling cacheConfigsRepo.get(serverName)
per entry. In the Redis aggregate-key backend, get() is implemented
as getAll() then map lookup, so N concurrent per-server lookups
inflate into N full-map reads and deserializations on every
ensureConfigServers pass.
The loop now takes a single getAll() snapshot at the top and hands
it into a synchronous isUnmodifiedYamlServer helper, turning O(n)
remote reads into O(1) regardless of how many MCP entries are
resolved. The snapshot also gives the unchanged-YAML comparison
one consistent view of YAML across all entries.
Regression test spies on cacheConfigsRepo.get and asserts it is
never called from ensureConfigServers, with getAll called exactly
once.
* feat: Add Bedrock API key support
* fix: Respect Bedrock credential mode
* fix: Support mixed Bedrock credential forms
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix(agents): normalize empty MCP tool descriptions to undefined
MCP servers (e.g. Asana MCP) can return tools with an empty string
description. AWS Bedrock's converse API rejects toolSpec.description
with length < 1, so any empty-description MCP tool caused the entire
request to fail with a validation error.
Convert empty strings to undefined at the two sites in
loadToolDefinitions where MCP tool definitions are built. An undefined
description is omitted from JSON serialization, so Bedrock never sees
the empty value. OpenAI and Anthropic direct APIs are unaffected.
Fixes#13209
* test(agents): add coverage for empty MCP tool description normalization
Verify that loadToolDefinitions converts '' descriptions to undefined
for both the sys__all__sys pattern and directly named MCP tools.
* 🏗️ refactor: Derive App Version from Root package.json + Add buildInfo Schema
The hardcoded `Constants.VERSION` in `data-provider` is now replaced at
rollup build time via `@rollup/plugin-replace`, sourcing from the root
`package.json` so version bumps are a single-file change.
Adds the shape needed by the rest of the series:
- `interface.buildInfo` boolean flag (default `true`) — lets self-hosters
opt out of exposing commit/branch/date.
- `buildInfo` on `TStartupConfig` — commit/commitShort/branch/buildDate.
- `SettingsTabValues.ABOUT` — new settings tab enum value.
Ref: https://github.com/danny-avila/LibreChat/issues/12406
* 🛠️ feat: Add Build Metadata Resolver and Expose via /api/config
Adds `resolveBuildInfo()` in `@librechat/api` that surfaces commit SHA,
branch, and build date from (in order) `BUILD_*` env vars, then local git
metadata. Result is cached per-process.
`/api/config` includes a `buildInfo` field on both authenticated and
anonymous responses when `interface.buildInfo !== false` and at least one
resolver field is populated. Omitted entirely otherwise.
Designed so pre-built Docker images carry metadata via build-arg while
source installs pick it up from `.git` — no manual version tracking.
Ref: https://github.com/danny-avila/LibreChat/issues/12406
* ℹ️ feat: Add Settings → About Panel with Diagnostics Copy
New Settings tab that renders the running build's version, commit (short
SHA), branch, and build date in a monospaced block alongside a "Copy
diagnostics" button that emits a preformatted text blob for pasting into
support issues.
Tab is hidden when `interface.buildInfo` is set to `false`. Reads from
`startupConfig.buildInfo` provided by `/api/config`.
Ref: https://github.com/danny-avila/LibreChat/issues/12406
* 🐳 ci: Inject BUILD_COMMIT/BRANCH/DATE into Docker Images
Adds optional `BUILD_COMMIT`, `BUILD_BRANCH`, `BUILD_DATE` ARGs to both
`Dockerfile` and `Dockerfile.multi`, wired as `ENV` vars in the runtime
stage so the backend's `resolveBuildInfo` picks them up.
All image-publishing workflows (`tag`, `main`, `dev`, `dev-branch`,
`dev-staging`) now compute `${github.sha}`, `${github.ref_name}`, and a
UTC timestamp, then pass them to `docker/build-push-action` as
`build-args`.
Defaults are empty — non-CI builds (local `docker build`) still work,
and the backend falls back to local `.git` metadata if ARGs aren't set.
Ref: https://github.com/danny-avila/LibreChat/issues/12406
* 📝 docs: Direct Bug Reporters to Settings → About for Version Info
The previous instructions (`docker images | grep librechat`,
`git rev-parse HEAD`) only worked for a subset of deployments and
rarely produced a commit SHA for users pulling pre-built images.
Point users to the new in-app Settings → About panel's
"Copy diagnostics" button, which captures version, commit, branch,
build date, and user agent in a single preformatted block. Fallback
instructions preserved for older installs.
Ref: https://github.com/danny-avila/LibreChat/issues/12406
* 🐳 fix: Move BUILD_* ENV to End of Docker Stages to Preserve Layer Cache
Per-commit BUILD_COMMIT/BUILD_DATE changes were being promoted to ENV
before `npm ci` / `npm run frontend` (single-stage) and before
`npm ci --omit=dev` (multi-stage api-build), which invalidated the cache
for every subsequent layer on every CI run.
Move the ARG/ENV block below the heavy install and build steps in both
Dockerfiles. Metadata is still available in the runtime image but no
longer busts layer reuse.
Addresses codex review on #12756.
* 🔧 fix: Propagate interface.buildInfo=false to Unauthenticated /api/config
The unauthenticated branch of `/api/config` was emitting an `interface`
object only when `privacyPolicy` or `termsOfService` was set, which
meant an admin's explicit `interface.buildInfo: false` opt-out was never
visible to anonymous/guest clients. `Settings.tsx` gates the About tab
on `startupConfig?.interface?.buildInfo !== false`, so a missing field
fell through as "enabled" for those clients.
Include `interface.buildInfo: false` in the unauth payload whenever it's
explicitly disabled. Keep the implicit default (true) absent to preserve
the minimal-unauth-payload convention.
Addresses codex review on #12756.
* 🔀 ci: Trigger Dev Image Workflows on Root package.json + Dockerfile Changes
The baked `Constants.VERSION` now reads from the root `package.json` via
rollup-plugin-replace, but the `dev-images.yml` and `dev-branch-images.yml`
path filters only matched `api/**`, `client/**`, `packages/**`. A release
commit that only bumps root `package.json` would not trigger a rebuild,
leaving `latest` dev images with stale Footer/About version metadata.
Include `package.json`, `package-lock.json`, and both Dockerfiles in the
path filters so dependency changes (lockfile rebuilds) and image build
tweaks also rebuild dev images.
Addresses codex review on #12756.
* 🧽 fix: Harden About Panel Lifecycle, A11y, and Loading Gate
Review follow-ups on #12756:
- #1 timer leak: stash the copy-state `setTimeout` in a ref and clear it
from a `useEffect` cleanup so unmounting the Settings dialog mid-toast
doesn't fire `setCopied(false)` on an unmounted component.
- #3 flash of About tab: gate `aboutEnabled` on `startupConfig != null`
so the tab stays hidden until `/api/config` returns. For admins who
disabled `interface.buildInfo`, the tab no longer briefly appears and
vanishes on page load.
- #6 aria-live placement: move the live region off the interactive
button onto a dedicated `<span role="status" aria-live="polite">` so
screen readers announce the copied state, not the full button content
on every re-render.
- #2 missing coverage: add `About.spec.tsx` exercising populated/empty
buildInfo rendering, invalid-date handling, diagnostics clipboard
payload, copy-state toggling, unmount cleanup, and the live region.
* ⚡ perf: Eagerly Resolve Build Info at Module Load
Review follow-up #4 on #12756: `resolveBuildInfo()` calls `execFileSync`
with a 2s timeout on source installs without `BUILD_*` env vars. Paying
this cost on the first HTTP request blocks the event loop mid-flight.
Call `resolveBuildInfo()` once at config route module load so the
resolver's cache is warm before any request arrives. Docker images with
the BUILD_* env vars set sidestep the git path entirely, so this only
affects the edge case of source installs.
* 📝 docs: Document rollup Version Placeholder Contract
Review follow-ups #5 / #8 on #12756. The `__LIBRECHAT_VERSION__`
placeholder relies on a substring replacement rule that only works
because the token appears inside a string literal, and the substitution
only runs during `npm run build:data-provider`.
- Expand the `Constants.VERSION` JSDoc to spell out that consumers read
the placeholder through the built dist bundle; source-level test
imports would see the raw placeholder.
- Add a NOTE above the rollup `replace` config warning future
contributors not to repurpose the token as a bare identifier without
switching to a quoted replacement value.
Non-functional; prevents future contributors from stepping on a subtle
constraint.
* 🪪 fix: Only Toast "Copied" When Clipboard Copy Actually Succeeds
Codex R5 on #12756. `copy-to-clipboard` returns a boolean indicating
whether the underlying `execCommand('copy')` / fallback prompt actually
wrote to the clipboard. The previous handler flipped to the "Copied"
state unconditionally, which in hardened browsers or when the
permission prompt is dismissed would mislead users into filing bug
reports without the diagnostics blob attached.
Gate the state/timer/live-region on the boolean return; silently no-op
on failure rather than showing a false positive. Adds a test asserting
the button label stays at "Copy diagnostics" when the clipboard call
fails.
* 🐳 fix: Derive main image metadata from checkout
* 🪪 fix: Keep About enabled until disabled
* ✅ test: Avoid literal Settings mock text
* 🧱 refactor: Rename Build Info Module
* fix: allow OpenID PKCE authentication without client secret
* Linting
* Strategy fix
* fix(openid): trim secret gates and add PKCE client metadata tests
* chore(openid): normalize spec line endings
* ⚡ perf: Short-Circuit Config Override Resolution for Empty Principals (#12549)
Skip the getApplicableConfigs DB query when buildPrincipals returns
an empty array, since there are no principals to match against.
* ⚡ perf: Separate Error Handling for Principal Resolution vs Config Overrides (#12550)
Distinguish between buildPrincipals and getApplicableConfigs failures
so the uncached fallback to baseConfig is intentional and logged
separately from config override errors.
* Revert "⚡ perf: Separate Error Handling for Principal Resolution vs Config Overrides (#12550)"
This reverts commit 860d41da910befeddd7c79ecaeb382cb6f8edcb1.
* Revert "⚡ perf: Short-Circuit Config Override Resolution for Empty Principals (#12549)"
This reverts commit 6bc8f9d9174640e97db14cea0d4f3126b5cb2865.
---------
Co-authored-by: CMF\e-leite <EduardoLeite@criticalmanufacturing.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
MCP OAuth access tokens are stored with a 365-day default expiry when the
provider's token response omits `expires_in` (only RECOMMENDED per RFC 6749
§5.1). Providers that issue short-lived JWT access tokens but omit
`expires_in` (e.g. Salesforce) therefore get tokens treated as valid for a
year and never refreshed, so every call 401s once the real token lapses
until the user manually reconnects.
When the access token is a JWT (RFC 9068), read its `exp` claim and use it as
the authoritative expiry, falling back to the 365-day default only for opaque
tokens. Explicit `expires_at`/`expires_in` still take precedence.
Adds unit tests for storeTokens expiry resolution.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
There are two ways to add a file to a conversation:
1. Uploading a new file.
2. Using an existing file (from the side panel).
If you decide to remove the file, the behavior differs depending on
how it was added. If you just uploaded a new file, it gets deleted
from the conversation & the system. But if it's an existing file,
then it only gets removed from the conversation (but not deleted).
However, in both cases, it would show a toast saying that the file
was deleted, which is incorrect for the "existing file" case.
Now we check whether the file is `attached` (to the system) before
showing the deletion toast, and skip showing it if we're not actually
deleting the file.
* feat: support data retention for normal chats
Add retentionMode config variable supporting "all" and "temporary" values.
When "all" is set, data retention applies to all chats, not just temporary ones.
Adds isTemporary field to conversations for proper filtering.
Adapted to new TS method files in packages/data-schemas since upstream
moved models out of api/models/.
Based on danny-avila/LibreChat#10532
Co-Authored-By: WhammyLeaf <233105313+WhammyLeaf@users.noreply.github.com>
(cherry picked from commit 30109e90b04c52a7a033986b72fb054b045accf1)
* feat: extend data retention to files, tool calls, and shared links
Add expiredAt field and TTL indexes to file, toolCall, and share schemas.
Set expiredAt on tool calls, shared links, and file uploads when
retentionMode is "all" or chat is temporary.
(cherry picked from commit 48973752d353fcef5c68dee9d17344afd54aee9c)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: lint/test
(cherry picked from commit 310c514e6ae846a58c305ca8d899492f829e64cc)
* fix: address code review feedback for data retention PR
Critical:
- Fix BookmarkMenu crash: restore optional chaining on conversation
- Fix migration hazard: backward-compatible sidebar filter that also
checks expiredAt for documents without isTemporary field
Major:
- Add logging to getRetentionExpiry error path, align with tools.js
- Add tests for retentionMode: ALL in saveConvo and saveMessage
- Fix share route: apply expiredAt for temporary chats too by
querying the conversation's isTemporary flag server-side
- Add assertions for getRetentionExpiry mocks in process tests
Minor:
- Fix ChatRoute isTemporaryChat to be strictly boolean via Boolean()
- Fix stale test description (expired -> temporary)
- Comment out retentionMode default in example yaml
- Simplify verbose if/else to isTemporary === true
- Add compound index on { user: 1, isTemporary: 1 }
- Remove narrating comment from process.spec.js
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6bad535f901922e4a89e08fb925ac06c1f0bd03b)
* chore: fix typescript
(cherry picked from commit 826527a46b95e769f5ee8095289a309968a68562)
* fix: lint
(cherry picked from commit 77817e80ea2e1c06e1321b9c85b38786022f039e)
* fix: use mockSanitizeArtifactPath in retention test
The 'getRetentionExpiry is called with the request object' test
referenced an undefined `mockSanitizeFilename` identifier, breaking
both lint (no-undef) and the test suite. Use the existing
`mockSanitizeArtifactPath` mock that the surrounding tests already
use, since `processCodeOutput` calls `sanitizeArtifactPath` (not
`sanitizeFilename`) before invoking `getRetentionExpiry`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 52ea2da66d700fb4f6db91871c9d6d9acf1aab69)
* fix: forward isTemporary from client for retention on file uploads and tool calls
Server-side `getRetentionExpiry` (file uploads) and the tool-call
controller both read `req.body.isTemporary`, but the file upload
multipart form and the tool-call payload did not include that field.
In `retentionMode: temporary` (default), files uploaded and tool
calls created from temporary chats were therefore retained
indefinitely.
Forward the Recoil `isTemporary` flag in both client paths so the
existing server checks can fire correctly. `ToolParams` gains an
optional `isTemporary` field.
Addresses Codex P1 review feedback on PR #29.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7e937df05ad3bd4f958e53c21b42084f80a151d2)
* test: stub store.isTemporary in useFileHandling test mocks
Previous commit added `useRecoilValue(store.isTemporary)` to the
hook. The test file mocks `~/store` with only `ephemeralAgentByConvoId`
and does not stub `useRecoilValue`, so all 7 cases threw
"Invalid argument to useRecoilValue: expected an atom or selector but
got undefined". Add a stub default export with `isTemporary` and a
`useRecoilValue` mock returning `false`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit eb1609537d7d8a13be20880d7c13ecff37b52686)
* fix: harden data retention semantics
* fix: provide sweep request context for expired files
* fix: preserve temporary flags in all-retention updates
* fix: honor assistant versions in retention sweeps
* fix: retain non-temporary flags in all mode
* fix: hide expired retained records
* fix: propagate retained conversation expiry
* fix: refresh meili retention cutoff
* fix: prevent overlapping file sweeps
* fix: show legacy retained conversations
* fix: index legacy retained records
* fix: harden retention cleanup edge cases
* fix: count failed file storage sweeps
* fix: preserve legacy temporary retention
* fix: assign retention sweep worker deterministically
* fix: hide expired shared links on reads
* fix: prevent retention refresh after parent expiry
* fix: break code output retention import cycle
* fix: harden retention review findings
* fix: ignore expired share duplicates
* fix: reject expired retained share creation
* fix: harden retention review edge cases
* fix: address retention audit findings
* fix: enforce expired conversation shares in all retention
* fix: scope temporary upload flag to chat files
* fix: address retention review findings
* fix: address codex retention review findings
* fix: tighten missing storage detection
* test: remove unused file process spec bindings
---------
Co-authored-by: WhammyLeaf <233105313+WhammyLeaf@users.noreply.github.com>
Co-authored-by: Aron Gates <aron@muonspace.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per global rule: /v1/ only, never /api/. Internal API calls + handler
comments + route registrations updated. External vendor APIs (Stripe,
KMS, etc.) left as-is. LibreChat upstream backend routes (api/server/*)
left as-is per "leave upstream-vendored paths" guideline; data-provider
SDK now points at Hanzo Cloud Gateway /v1/* canonical paths since
api.hanzo.ai is our gateway and /api/ on top of api.* is double-prefix.
* 📦 chore: npm audit fix 2026-05-18
- Added @js-sdsl/ordered-map version 4.4.2
- Updated @librechat/agents to version 3.1.87
- Upgraded @opentelemetry/sdk-node to version 0.218.0
- Added new dependencies for gRPC and OpenTelemetry exporters
* 🔧 chore: Update @librechat/agents to version 3.1.87 in package-lock.json and package.json files
* 🔧 chore: Upgrade @opentelemetry/sdk-node to version 0.218.0 in package.json and package-lock.json
Replace the (a: any) cast and its eslint-disable directive in
ServerConfigsDB.getAll() with a precise lean-projection generic.
The Mongoose query already projects only mcpServerNames, so we
can use Pick<IAgent, 'mcpServerNames'> and let .lean<T[]>() carry
the shape through without resorting to any.
Switches the empty-array guard from || to ?? for clarity; both
behave identically for string[] | undefined.
Aligns with the project's "Never use any" rule (CLAUDE.md ->
Type Safety). No runtime change.
Collapse the transitional VITE_HANZO_IAM_{URL,APP,ORG} env aliases
to the canonical VITE_IAM_{URL,APP,ORG}. Aligns with the ecosystem
IAM_* convention (no HANZO_ prefix on identity vars).
Four jest mocks for `winston` in the test suite return the wrong shape:
api/test/__mocks__/logger.js (returns inner fn directly)
packages/api/src/agents/__tests__/memory.test.ts (`format` is a plain object)
packages/api/src/agents/__tests__/run-summarization.test.ts (same)
packages/api/src/agents/__tests__/initialize.test.ts (same)
Real `winston.format(fn)` returns a Format constructor whose instances
expose a `.transform(info, opts)` method that winston's pipeline calls
with the log info object. The current mocks collapse this:
- `(fn) => fn` returns the inner transform fn directly. When module-load
code in `@librechat/data-schemas/dist/config/parsers.cjs:52` does
`const redactFormat = winston.format((info) => ...)`, `redactFormat`
becomes the inner fn. The next line in `winston.cjs` calls
`parsers.redactFormat()` which invokes the inner fn with no `info`,
throwing `TypeError: Cannot read properties of undefined (reading 'level')`.
- `format: { combine, colorize, simple }` makes `winston.format` not
callable at all — `winston.format((info) => ...)` throws
`TypeError: winston.format is not a function`.
These currently pass in CI on GitHub Actions Ubuntu / Node 20.19, but
fail reproducibly on Node 24.x and on some Linux distros (verified on
WSL Ubuntu with Node 24.9.0). The CI passes appears to be environmental
luck around jest's mock-hoisting interaction with the workspace symlink
chain — the mocks are genuinely wrong against the data-schemas contract.
The fix: return a thunk that yields `{ transform: fn }` — matches real
winston's shape just enough that module-load completes; the inner fn is
only ever invoked by winston's pipeline (never at load time). Also adds
the full `winston.format.*` method surface (printf, timestamp, errors,
splat, json) plus `addColors` and the `DailyRotateFile`/`File` transports
that data-schemas's dist code references at module-load.
Verification (Node 24.9.0):
npm run build:data-provider && npm run build:data-schemas && npm run build:api
cd packages/api && npx jest src/agents/__tests__/{memory,run-summarization,initialize}.test.ts
→ 3 suites, 106 tests, all pass
No production code or behavior changes — test-only patch.
Co-authored-by: Jorge Costa <8352477+JorgeCosta87@users.noreply.github.com>
`streamProcessingMode` affects how guardrail processes the stream from
the model. If it's in "sync" mode, it chunks up the response and processes
them before returning them to the user. If it's in "async" mode, it
both processes the chunk & sends it to the user at the same time, allowing
for smoother streaming (at the cost of guardrail only reacting *after*
offending content starts to stream, in some cases).
* 📦 chore: Bump `@librechat/agents` to v3.1.86 in package-lock.json and package.json files
* 📦 chore: Update dependencies in package-lock.json to latest versions, including @protobufjs/codegen, @protobufjs/inquire, @protobufjs/utf8, and protobufjs
* 📦 chore: Add `librechat-data-provider` dependency in package.json and package-lock.json, and update build dependencies in turbo.json
* feat(cloudfront): add requireSignedAccess to enforce strict signed access
Introduces cloudfront.requireSignedAccess (default false). When enabled,
initializeCloudFront requires both CLOUDFRONT_KEY_PAIR_ID and
CLOUDFRONT_PRIVATE_KEY, rejects the unimplemented imageSigning="url"
mode, and initializeFileStorage throws to block startup on any
CloudFront init failure. OSS path is unchanged: missing keys still
log-and-continue when requireSignedAccess is false.
Adds low-noise startup and cookie-issuance logs without leaking signed
URLs, policies, signatures, private keys, or cookie values.
* fix(cloudfront): reject requireSignedAccess unless imageSigning is "cookies"
Previously requireSignedAccess=true was accepted with imageSigning="none"
or "url", but setCloudFrontCookies() only runs for "cookies" — leaving
strict mode toothless: CloudFront stayed publicly accessible, or image
delivery broke on a distribution that actually requires signed access.
Adds a Zod refinement plus a runtime guard in initializeCloudFront so
the only currently-functional strict configuration is imageSigning
"cookies". Signed URL mode can lift this restriction once implemented.
* fix(cloudfront): resolve strict access type checks
* chore(cloudfront): reduce strict startup log noise
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 📦 chore: Update @librechat/agents to version 3.1.85 in package-lock.json and package.json files
* 📦 chore: Update mermaid to version 11.15.0 in package.json and package-lock.json
`config/set-balance.js` calls `getBalanceConfig()` without an argument,
so it cannot read the parsed `balance` section from librechat.yaml.
As a result the script always errors with "Balance is not enabled.
Use librechat.yaml to enable it" — even when balance is enabled.
`config/add-balance.js` already follows the correct pattern (fetch
`appConfig` via `getAppConfig()` and pass it into `getBalanceConfig`),
introduced in #9234. This applies the same pattern to set-balance.js.
Co-authored-by: Odrec <Odrec@users.noreply.github.com>
* refactor: Streamline batch upload error handling in `uploadCodeEnvFile`
* refactor: Enhance session info error logging in `getSessionInfo`
* refactor: Update error logging to use `logAxiosError` in various agent handlers and skill file processing functions
* refactor: Consolidate missing resource checks in `createToolExecuteHandler` for better clarity
* fix: skip OpenAI model fetch if using user-provided key
There was a check present (via `opts.userProvidedOpenAI`), but it wasn't
working because `loadDefaultModels()` doesn't provide that parameter. As a
result, the server would repeatedly try to request models from OpenAI and get
401 errors in return.
We now check the env var directly, which matches how
`getAnthropicModels()` works.
* chore: remove unused OpenAI model option
* fix: honor explicit OpenAI key for model fetch
* fix: fall back from empty OpenAI option key
---------
Co-authored-by: Dan Lew <daniel@mightyacorn.com>
* 🛟 fix: Allow empty modelSpecs.list to unstick admin-panel saves
The unconditional `.min(1)` on `specsConfigSchema.list` rejected an empty
list even when `enforce: false`, leaving admin panels (which save fields
path-granularly) with no atomic way to clear the list once it had been
populated. Once an admin reached `list: [entry]` and deleted the only
entry, every subsequent save failed schema validation and the section
became stuck.
Relax the schema to `.default([])`. The `.min(1)` was added in #5218 as
part of bundled cleanup, not as a deliberate rule. Every consumer of
`modelSpecs.list` already handles the empty/undefined case (`?.list`,
`?? []`, length-checked), and `processModelSpecs` short-circuits to
`undefined` when the list is empty so the runtime treats it as "no
specs configured." No call site is load-bearing on length >= 1.
Tighten the `buildEndpointOption.js` enforce guard from
`?.list && ?.enforce` to `?.list?.length && ?.enforce`. Empty arrays
are truthy in JS, so the existing guard would have entered the enforce
branch on `list: []` and returned "No model spec selected" or "Invalid
model spec" had `processModelSpecs` ever been bypassed.
Add a runtime warn in `processModelSpecs` when `enforce: true` is
configured alongside an empty list, so operators see the resulting
"enforcement disabled" state in logs rather than silently getting a
permissive runtime.
Add coverage for the empty-list parse path in `config-schemas.spec.ts`
and for the empty-list-with-enforce branch in `buildEndpointOption.spec.js`.
* chore: update import order in config-schemas.spec.ts
`resolveSummarizationProvider` calls `getProviderConfig` to translate the
agent's resolved provider into an initializer + client overrides. Three
real-world inputs were unsupported and fell through to "raw provider"
fallback (silently dropping client overrides):
1. **`vertexai`** — not in `providerConfigMap` at all. Vertex shares
initialization with Google (auth-only runtime distinction). Map
`Providers.VERTEXAI` to `initializeGoogle`.
2. **`openrouter` (and other known custom providers) with CamelCase
custom endpoint names** — agent main flow looks up endpoints
case-sensitively (case-preserving keys are how
`loadCustomEndpointsConfig` lets users have distinct entries
differing only in case). Once it succeeds, `agent.provider` is
normalized to lowercase. Downstream resolvers re-enter
`getProviderConfig` with the lowercased value and miss configs
whose `name` is camel-cased. Add a case-insensitive fallback,
narrowly scoped to known custom providers and only after the
case-sensitive direct lookup fails.
3. **Ambiguous case-insensitive matches (codex review feedback)** —
if the user has e.g. `OpenRouter` and `OPENROUTER` (neither
lowercase) and the agent runtime passes `openrouter`, the
case-insensitive fallback could silently route to whichever entry
appears first in the array (potentially different baseURL/apiKey).
Detect multiple case-insensitive matches and throw a clear error
with both names rather than picking arbitrarily.
## Tests
`providers.spec.ts` — new file, 7 tests:
- vertexai → Google initializer
- google (API key) → Google initializer (regression guard)
- case-insensitive fallback when only CamelCase entry exists
- exact-case match preserved when both casings exist (case identity)
- exact-case lowercase entry still resolves
- throws on ambiguous case-insensitive matches when no exact-case exists
- still throws when no match at all
When a tool round-trip is interrupted between the tool result and the
model's text reply (user aborted, network drop, pod restart, ...) and
LibreChat persists the partial assistant message, the next conversation
turn reconstructs an `AIMessage` from `formatAgentMessages` that has
`tool_calls` populated but no `additional_kwargs.signatures`. Vertex
Gemini 3 rejects the resumed request with 400 because the most recent
historical functionCall has no `thought_signature`.
## Storage shape
Capture as `Record<tool_call_id, signature>` rather than a flat array.
This addresses the codex P1 review:
> When an assistant turn contains multiple sequential tool-call batches,
> this restoration path writes all persisted thoughtSignatures onto only
> the last tool-bearing AIMessage. Vertex/Gemini validates signatures
> for each step in the current tool-calling turn, so earlier
> functionCall steps reconstructed without their signature can still
> fail with 400.
A single agent run can fire multiple `chat_model_end` events when the
loop cycles the LLM with intervening tool results — each cycle owns a
distinct `tool_call_id`. Per-id storage maps each signature back onto
the right reconstructed `AIMessage`, not just the last one.
## Mapping
`additional_kwargs.signatures` is a flat array indexed by *response part*
(text + functionCall interleaved). `tool_calls` is just the function
calls in their original order. Non-empty signatures correspond 1:1 with
tool_calls in order — see `partsToSignatures` in
`@langchain/google-common`. Single-pass walk maps `signatures[i]` (when
non-empty) onto the i-th `tool_call.id`.
## Pipeline
| Stage | File | Change |
|---|---|---|
| Capture | callbacks.js | `ModelEndHandler` accepts `Record<string,string>` map; walks signatures + tool_calls in tandem to record per-id. Gated on the map being provided — non-Vertex flows are no-op (and also no-op even when provided, since they don't emit signatures). |
| Plumbing | initialize.js | Allocate `collectedThoughtSignatures = {}`, share with handler + client. Always allocated; the JSDoc explicitly documents that it stays empty for non-Vertex providers. |
| Surface | client.js | `sendCompletion` returns `metadata.thoughtSignatures` when the map has entries; falls through unchanged when empty. |
| Persist | (existing BaseClient.handleRespCompletion) | Writes `metadata` from `sendCompletion` onto `responseMessage.metadata`. Mongoose `Mixed` — no migration. |
| Restore | formatMessages.js | Track every tool-bearing AIMessage produced from a TMessage. For each, build a position-aligned `additional_kwargs.signatures` array (empty placeholders for tool_calls without a stored sig). Agents' `fixThoughtSignatures` dispatches non-empty entries to functionCall parts in order. |
## Live verification
- **Single-step:** real Vertex `gemini-3.1-flash-lite-preview` resume-after-tool case. With fix ✅ / without ❌ 400.
- **Multi-step (codex case):** real two-step agent loop (list /tmp → echo done). Each step's signature attaches to its own reconstructed AIMessage. With fix ✅ / without ❌ 400.
- **Cross-provider:** Anthropic Claude haiku-4.5 + OpenAI gpt-5-mini accept the persisted/restored shape unchanged.
## Tests
`modelEndHandler.spec.js` (new) — 6 tests:
- maps non-empty signatures onto tool_call_ids in order
- accumulates per-id across multiple `model_end` events (multi-step)
- no-op when `collectedThoughtSignatures` is null
- no-op when `signatures` field missing (non-Vertex)
- no-op when `tool_calls` missing
- preserves existing `collectedUsage` array contract
`formatAgentMessages.spec.js` — 6 new tests:
- restores onto the AIMessage that owns the tool_call
- per-step attachment for multi-step turns (codex review case)
- preserves tool_call ordering when signatures are partial
- no-op when metadata.thoughtSignatures absent
- no-op when assistant has no tool_calls
- no-op when stored ids don't match any current tool_call
37 passing across 3 suites; 15 existing formatAgentMessages tests unchanged.
## Compatibility
- Backward-compatible — restore gated on `metadata.thoughtSignatures` being a populated object; capture gated on the map being provided.
- No schema migration — uses `Message.metadata: Mixed` already in place.
- Cross-provider safe — non-Vertex providers tolerate the field (verified live against Anthropic + OpenAI converters).
- Pairs with [agents#159](https://github.com/danny-avila/agents/pull/159) for full coverage on histories that mix plain-text and toolcall AIMessages.
Bitnami moved versioned image tags from docker.io/bitnami to
docker.io/bitnamilegacy on 2025-08-28, which causes ImagePullBackOff
on a fresh Helm install of the LibreChat chart. Override the MongoDB
subchart image repository to bitnamilegacy/mongodb so installs
succeed out of the box.
Fixes#13031
* feat(admin-panel): add /api/admin/oauth/refresh endpoint for cross-origin BFF refresh
The cookie-based /api/auth/refresh controller can't be reached cross-origin
from a separately-hosted admin panel because the refresh-token cookie isn't
sent on cross-origin fetches. Add a dedicated POST /api/admin/oauth/refresh
endpoint that accepts the refresh token in the request body, exchanges it
at the IdP via openid-client refreshTokenGrant, and returns the same
response shape as /api/admin/oauth/exchange.
Implementation lives in packages/api/src/auth/refresh.ts as the
applyAdminRefresh helper. It validates the refreshed tokenset, looks up the
admin user by openidId (with optional user_id disambiguation when multiple
user docs share an openidId), mints the bearer via an injected mintToken
hook, and runs an optional onRefreshSuccess hook for downstream forks that
need to update server-side session state.
The default mintToken passed by the OSS route signs an HS256 LibreChat JWT
via generateToken so admin panel callers continue to use the existing local
JWT strategy. Forks that prefer to hand back an IdP-signed token (e.g. for
deployments where the JWT auth gate is JWKS-only) override mintToken
without changing the helper or the route.
Also threads expiresAt through AdminExchangeData and AdminExchangeResponse
so admin panel clients can drive proactive refresh before the bearer
expires. Defaults the OSS exchange flow to Date.now() + sessionExpiry.
* fix(admin-panel): address review feedback on /api/admin/oauth/refresh
mintToken now returns {token, expiresAt} so the minter is authoritative
for the bearer's lifetime instead of deriving it from the IdP `exp` claim.
The refresh response would otherwise lie to the admin panel and trigger
premature or late refresh cycles.
The helper now falls back to the inbound refresh_token when the IdP omits
one on rotation (Auth0 with rotation off, Microsoft personal accounts).
Without this the admin panel loses its refresh capability after one cycle.
Other hardening:
resolveAdminUser validates user_id with Types.ObjectId.isValid before
hitting Mongoose, avoiding a CastError that would surface as a generic
500 with no useful information for the client.
If user_id resolves to a user whose openidId does not match the refreshed
sub, throw USER_ID_MISMATCH (401) instead of silently swapping in a
different user matching the sub.
Wrap tokenset.claims() in readClaims so an IdP that returns a tokenset
without a usable id_token gets mapped to CLAIMS_INCOMPLETE (502) rather
than bubbling a raw exception.
findUsers now uses the same SAFE_USER_PROJECTION as getUserById so the
fallback path no longer pulls password/totpSecret/backupCodes into memory.
Removed dead fields (email on AdminRefreshClaims, id_token on
RefreshTokenset) and fixed import ordering per AGENTS.md.
Adds packages/api/src/auth/refresh.spec.ts: 18 tests covering the happy
path, userId disambiguation (match, invalid ObjectId, null, mismatch),
all error branches (IDP_INCOMPLETE, CLAIMS_INCOMPLETE for both throw and
missing sub, USER_NOT_FOUND, mintToken/onRefreshSuccess propagation), and
refresh-token preservation under rotation/no-rotation.
* chore(admin-panel): polish per re-review on /api/admin/oauth/refresh
readClaims now logs the original error name/message at warn before mapping
to CLAIMS_INCOMPLETE so a programming bug doesn't get silently rebadged
as an IdP problem in production logs.
The route handler's JSDoc now enumerates every error response (status +
error_code) so admin-panel implementors can plan for each branch without
reading the source.
Tightens the helper's surface: removed the now-dead `exp` field from
`AdminRefreshClaims` (only `sub` is read since the v2 mintToken refactor),
and tightened `AdminRefreshDeps.findUsers`'s projection parameter from
`string | null` to `string` so the contract matches actual usage.
Test polish: the userId-resolves-to-null fallthrough test now asserts the
exact `findUsers` and `getUserById` call arguments so a regression in the
fallthrough query shape is caught. The "skips onRefreshSuccess" test now
asserts a populated response shape rather than just `toBeDefined`.
Declined per prior triage and re-confirmed: a role guard inside
`applyAdminRefresh` (downstream `/api/admin/*` already enforces
ACCESS_ADMIN via requireCapability) and moving the IdP grant call out of
the JS route into TypeScript (matches existing oauth.js / openidStrategy
pattern; package-boundary refactor belongs in a separate PR).
* fix(admin-panel): reject /api/admin/oauth/refresh tokensets from foreign issuers
When the route handler can resolve the configured OpenID issuer, it now
threads it into applyAdminRefresh as expectedIssuer. The helper compares
that against the tokenset claims iss (after normalizeOpenIdIssuer on
both sides to absorb trailing-slash differences) and throws
ISSUER_MISMATCH (401) on mismatch.
The check is skipped when either side is unset so behavior is unchanged
for IdPs that don't return iss on a refresh-grant id_token, and for
older deployments where the OpenID config doesn't expose serverMetadata.
This is a defense-in-depth measure for the refresh path only. The
deeper OIDC posture fix (binding IUser lookup to (sub, iss) as a pair)
is pre-existing debt across openidStrategy.js and the regular exchange
flow as well, and belongs in a separate PR with the schema change and
backfill migration.
* fix(admin-panel): bind refresh user lookup to (sub, iss) and handle getOpenIdConfig throw
Two fixes raised on the PR thread that I previously misdescribed:
The user lookup in resolveAdminUser was keyed on openidId alone, so a
tokenset from a different issuer that happened to share the same sub
could resolve to a local user from a different IdP. Now exports
getIssuerBoundConditions and isUserIssuerAllowed from openid.ts (the
helpers findOpenIDUser already uses) and reuses them. The findUsers
filter becomes ($or of getIssuerBoundConditions for openidId) when an
expectedIssuer is provided, with the same legacy backward-compat
clause for users whose openidIssuer field was never populated. The
direct user_id path now also checks isUserIssuerAllowed and throws
USER_ID_MISMATCH if the stored openidIssuer disagrees with the
configured issuer.
The route's getOpenIdConfig() call was previously documented as
returning null when uninitialized; the actual implementation throws.
That made the if (!openIdConfig) guard unreachable, and an unconfigured
server would surface as 500 INTERNAL_ERROR rather than 503
OPENID_NOT_CONFIGURED. Wraps the call in try/catch so the documented
503 response is what callers actually receive.
Adds 4 tests covering the new lookup binding behavior.
* fix(admin-panel): re-check ACCESS_ADMIN on /api/admin/oauth/refresh
The IdP refresh token can outlive a capability/role change, so the
initial requireAdminAccess on the OAuth callback isn't sufficient.
Inject canAccessAdmin via the existing capability model
(hasCapability with SystemCapabilities.ACCESS_ADMIN, matching
requireAdminAccess so custom roles and user grants are honored)
and gate token minting on it. Capability backend errors are
warn-and-denied to keep the bearer-mint path fail-closed.
* fix(admin-panel): scope /api/admin/oauth/refresh to the request tenant
The same (openidId, openidIssuer) pair is allowed across tenants by
the user schema's unique index. The refresh helper was wrapping both
the direct getUserById and the fallback findUsers in runAsSystem,
bypassing tenant isolation, so an IdP identity that exists in two
tenants could resolve to the wrong tenant's user and mint a JWT
bound to that tenant.
Drop the runAsSystem wrappers, add a trusted tenantId option to
applyAdminRefresh, AND it into the fallback findUsers filter, and
assert it against the direct getUserById result. Mount
preAuthTenantMiddleware on the refresh route so the deployment's
X-Tenant-Id header drives the trusted tenant via ALS. Single-tenant
deploys (no header) keep the existing openidId-only behaviour.
Adds TENANT_MISMATCH (401) and a regression covering duplicate
(sub, iss) across tenants plus the direct-userId tenant assertion.
* fix(admin-panel): gate /api/admin/oauth/refresh on OPENID_REUSE_TOKENS
The OSS refreshController only refreshes OpenID tokensets when
OPENID_REUSE_TOKENS is enabled. The body-based admin variant was
unconditionally calling refreshTokenGrant, which made the flag
ineffective for the admin OAuth flow and let admin sessions keep
renewing in deployments that explicitly turned token reuse off.
Add the same isEnabled(process.env.OPENID_REUSE_TOKENS) check up
front and return 403 TOKEN_REUSE_DISABLED so the admin panel BFF
can surface the configuration mismatch instead of silently churning
through retries.
Code-execution outputs land on `messages.attachments` (set by
`processCodeOutput`), while user uploads land on `messages.files`.
The threadFileIds switch (#13004) walked only `files`, so on a
single linear thread:
Turn 1: assistant produces sample.xlsx → attachment with codeEnvRef
Turn 2: user says "add 2 rows"
→ primeCodeFiles: file_ids=0 resourceFiles=0
→ /exec sent files=[]
→ sandbox: FileNotFoundError: 'sample.xlsx'
The `getThreadData` walk found zero file_ids because the assistant's
codeEnvRef was on `attachments`, not `files`. Compounded by the
DB select string `'messageId parentMessageId files'` which didn't
pull `attachments` into memory in the first place — so even fixing
the walk in isolation wouldn't have surfaced them.
Both layers fixed:
- `ThreadMessage` type adds `attachments?: Array<{ file_id?: string }>`
- `getThreadData` walks both arrays, dedups via the same Set
- `initialize.ts` selects `'messageId parentMessageId files attachments'`
## Test plan
`packages/api/src/utils/message.spec.ts` (+6 cases):
- collects file_ids from `attachments`
- walks both `files` and `attachments` on the same message
- regression: linear thread with code-output attachments across
user→assistant→user→assistant produces the right file_ids
- dedupes shared ids that appear in both arrays
- skips attachments without file_id (mirrors `files` behavior)
- empty `attachments` array
`packages/api/src/agents/__tests__/initialize.test.ts` (+1 case):
- locks the DB select string includes `attachments` alongside
`files` / `messageId` / `parentMessageId`
- [x] `npx jest src/utils/message.spec.ts` — 39/39 pass
- [x] `npx jest src/agents/__tests__/initialize.test.ts` — 33/33 pass
- [x] lint clean on all four touched files
* refactor(attachments): add variant prop to AttachmentGroup
* feat(tool-call): add hideImageAttachments prop to ToolCall
* fix(tool-call): keep MCP image outputs visible when tool group auto-collapses
* test(tool-call): verify MCP images hoist out of collapsed tool group
* fix(tool-call): hoist all grouped attachments and prevent ExecuteCode double-render
- rename hideImageAttachments -> hideAttachments and hide every attachment
in the inner tool when a group auto-collapses, then hoist them via
ToolCallGroup with default variant 'all' so non-image attachments survive
the collapse alongside images
- thread hideAttachments to ExecuteCode so it skips its inline AttachmentGroup
when grouped, preventing double-render when the group is expanded
- memoize sequentialParts and groupedParts in ContentParts (with
groupAttachments rolled into each tool-group entry) so we don't re-flatMap
on every render
* test(tool-call): cover hideAttachments contract and grouping integration
- ToolCall: assert AttachmentGroup is skipped when hideAttachments=true and
rendered when explicitly false, locking the prop's contract
- ToolCallGroup: update variant assertion to 'all' (now hoists images and
files together) and add a non-image-only hoist case
- ContentParts.integration: new test exercising the full
ContentParts -> Part -> ToolCall -> AttachmentGroup chain with realistic
MCP-shaped data (groups 2+ contiguous tool calls and hoists, single calls
render inline, mixed image+file hoists, empty attachments are a no-op)
* fix(tool-call): extend hideAttachments to bash/read_file/skill/subagent
When the post-rebase dev branch added BashCall, ReadFileCall, SkillCall,
and SubagentCall as dedicated tool renderers, each rendered its own
inline AttachmentGroup. Once the parent tool group hoists every
attachment, those inline groups would double-render, so they now honor
the same hideAttachments contract as ToolCall and ExecuteCode.
Also seed the new ToolCallGroup mocks (Users icon, getToolDisplayLabel)
so the existing hoist test suite keeps passing on dev.
* fix(image-gen): suppress inline image when attachments are hoisted
OpenAIImageGen renders the generated image directly via <Image>. When
its tool_call lands inside a grouped tool call, the parent now hoists
those attachments into ToolCallGroup's AttachmentGroup, and the inline
<Image> would render the same file a second time. Thread hideAttachments
through Part -> ImageGen (agent-style branch) so the agent-style image
slot stays out of the way once the parent has hoisted.
* refactor(tool-call): drop dead variant prop and flatten render-part hooks
- AttachmentGroup's variant prop ('images' / 'non-images') had no callers
after the final hoisting design landed, so remove the prop and the
filtering branches; everything passes the default 'all' behavior.
- Replace the makeRenderPart factory + dual useMemo with two plain
useCallbacks (renderPart, renderGroupedPart) sharing the same dep set.
- Tighten test mocks: drop 'any' in the new integration test, hoist the
MCP delimiter constant above its consumer, and remove the now-stale
data-variant attribute assertion.
* refactor(tool-call): extract getToolCallId helper and tidy imports
- Pull the (part?.[TOOL_CALL] as Agents.ToolCall)?.id chain into a single
getToolCallId helper in ContentParts so the three call sites stop
repeating the cast verbatim.
- Re-sort ToolCallGroup local imports longest-to-shortest per the project
convention.
- Add a Users mock to the integration test's lucide-react stub so future
subagent-group tests don't trip over an undefined glyph.
* refactor(tool-call): unnest ternaries in subagent and group labels
* 🐛 fix: anchor getCodeGeneratedFiles on threadFileIds, not threadMessageIds
In a branched conversation (regenerations producing the same code-output
filename), `getCodeGeneratedFiles` would silently exclude files whose
File-record `messageId` lived on a sibling branch. The user-visible
symptom: "the previous file isn't persisted" — the LLM tries
`load_workbook("output.xlsx")` on turn 2 and gets `FileNotFoundError`
because LC sent `_injected_files: []` to codeapi instead of priming
the prior turn's output.
`claimCodeFile` is keyed by `(filename, conversationId, context)` —
not by messageId. When sibling A first creates `output.csv`, the File
record persists with `messageId = A`. When sibling N (a regeneration
of A's parent) recreates `output.csv`, the claim finds A's record and
`processCodeOutput` deliberately preserves `messageId = A` to keep
file→original-creator provenance intact (correct behavior for the
linear case where the original creator is in-thread).
Turn N+1's `parentMessageId = N`. `getThreadData` walks back from N:
the thread is `[N, root]` — sibling A is NOT in it. The pre-fix query
filtered by `messageId IN [N, root]`, so the file was excluded.
`getCodeGeneratedFiles` already lives next to `getUserCodeFiles`,
which has always filtered by `file_id IN threadFileIds` (the file_ids
referenced by `messages.files[]` arrays during the thread walk). The
asymmetry — user-uploaded files anchored on the message's reference,
code-generated files anchored on the File's own creator — was the
bug. Anchoring both functions on `threadFileIds` reaches the right
files regardless of which sibling first generated them.
`File.messageId` stays informational ("who first generated this") for
provenance and `processCodeOutput`'s "preserve original messageId on
update" logic stays as-is — only the lookup key for thread-scoped
fetches changes.
- `packages/data-schemas/src/methods/file.ts`: signature + filter
change. JSDoc spells out the branched-conversation rationale.
- `packages/api/src/agents/initialize.ts`: pass `threadFileIds` instead
of `threadMessageIds`. The local `threadMessageIds` declaration is
removed since the only consumer is gone.
- `packages/data-schemas/src/methods/file.spec.ts`: 5 new cases:
- basic happy-path (file referenced by current thread)
- **the regression**: file's creator messageId is on a sibling
branch but file_id is in threadFileIds → finds it
- empty/missing threadFileIds returns []
- cross-conversation isolation
- non-execute_code context filter still applies (a chat attachment
won't be returned even if its file_id is in threadFileIds —
that's `getUserCodeFiles`'s job)
Applies cleanly on top of dev. When LC #12960 (the typed CodeEnvRef
cutover) lands, the only conflict is the legacy `metadata.fileIdentifier`
metadata key flipping to `metadata.codeEnvRef` — same line, trivial
resolve.
- [x] `cd packages/data-schemas && npx jest src/methods/file.spec` —
42/42 pass (including the 5 new regression cases)
- [x] `cd packages/api && npx jest src/agents` — 722/722 pass
(modulo 2 pre-existing summarization e2e failures unrelated)
- [x] `cd api && npx jest server/services/Files server/controllers/agents` —
432/432 pass
- [x] `npx tsc --noEmit -p packages/api/tsconfig.json` — clean
- [ ] Manual: branched conversation reproducer — generate a file in
turn 1, regenerate the parent (sibling), then in turn N+1 ask the
agent to read the file. Pre-fix: `FileNotFoundError`. Post-fix:
the file is primed and load_workbook succeeds.
* 🧪 test: lock initialize.ts → getCodeGeneratedFiles call shape
Integration-level regression test asserting initializeAgent passes
`threadFileIds` (not `threadMessageIds`) to getCodeGeneratedFiles
in branched-conversation scenarios. Locks in the API shape from the
previous commit, sitting one layer above the data-schemas unit test —
so a future refactor to the priming chain can't silently revert to
the messageId-based filter without surfacing a test failure here.
Two cases:
- The full call shape: agent.tools=['execute_code'], resendFiles=true,
threadData mock returns distinct messageIds and fileIds. Asserts the
call uses fileIds, and that getUserCodeFiles uses the same array
(the symmetric design that closes the sibling-branch hole).
- Empty threadFileIds: getCodeGeneratedFiles is still called with []
(its own internal early-return handles the empty case); getUserCodeFiles
is gated at the call site and stays unscheduled.
* 🧠 fix: charge Gemini reasoning tokens in agent usage accounting
Resolves#13006.
`usage.ts` previously billed `usage.output_tokens` directly. For Vertex
AI Gemini thinking models, `@langchain/google-common`'s streaming path
emits `output_tokens = candidatesTokenCount` only, dropping
`thoughtsTokenCount`. Reasoning was billed at zero and the
`total_tokens === input_tokens + output_tokens` invariant was broken.
The fix lives in agents (danny-avila/agents#157) — but this is also a
defense-in-depth backstop in case agents misses a path or another
provider exhibits the same shape. `resolveCompletionTokens(usage)` adds
`output_token_details.reasoning` back when (and only when) the gap is
present (`total - input > output`), so providers that already include
reasoning in `output_tokens` (OpenAI o-series, Anthropic, the
Google-API wrapper) are no-ops — no double-counting.
- `SplitUsage` gains a `completion` field; all four billing call sites
in `processUsageGroup` use it instead of `usage.output_tokens`.
- `total_output_tokens` in the result also reflects the corrected
count.
- `UsageMetadata` interface in `IJobStore.ts` adds the
`output_token_details` field for type safety.
- 4 new tests in `usage.spec.ts` cover: Vertex undercount fix, OpenAI
no-double-count, structured spend path with cache + reasoning, no-op
when no details present.
* 🩹 fix: simplify reasoning correction to invariant-based gap check
Initial fix gated the correction on `output_token_details.reasoning > 0`,
which doesn't help in the live failure case: when google-common's stream
emits the buggy fallback usage_metadata, output_token_details is empty
({}) and the gate exits early.
Live debugging showed the reliable signal is the documented invariant
itself: `total_tokens === input_tokens + output_tokens`. When buggy
streams undercount output, total exceeds input + output by exactly the
unbilled reasoning. Use `total - input` as the corrected output.
This is provider-agnostic and stays a no-op for compliant providers
(OpenAI/Anthropic/Google-via-CustomChatGoogleGenerativeAI), where the
gap is zero.
Live verified end-to-end against gemini-3-flash-preview:
- With agents fix in place: output_tokens=437 → billed 437 (no-op)
- Backstop only (no agents fix, buggy input): raw 135, billed 297
(= total 309 - input 12, matches actual API charge)
Updated tests to cover both scenarios.
* 🧱 refactor: typed CodeEnvRef + kind discriminator + tenant-aware sandbox cache
Final cutover for the LibreChat ↔ codeapi sandbox file identity. Replaces
the magic string `${session_id}/${file_id}?entity_id=...` with a typed,
discriminated `CodeEnvRef`. Pre-release lockstep deploy with codeapi
#1455 and agents #148; no legacy aliases retained.
## Final shape
```ts
type CodeEnvRef =
| { kind: 'skill'; id: string; storage_session_id: string; file_id: string; version: number }
| { kind: 'agent'; id: string; storage_session_id: string; file_id: string }
| { kind: 'user'; id: string; storage_session_id: string; file_id: string };
```
`kind` drives codeapi's sessionKey: `<tenant>:<kind>:<id>[✌️<version>]`
for shared kinds, `<tenant>:user:<userId>` for user-private (auth context
provides `userId`). `version` is statically required for `kind: 'skill'`
and forbidden otherwise via discriminated union — constraint holds at
compile time on every consumer, not just codeapi's runtime validator.
`id` is sessionKey-meaningful for `'skill'` / `'agent'`; informational
only for `'user'` (codeapi resolves user identity from auth context).
## What changed
- `packages/data-provider/src/codeEnvRef.ts` — discriminated union +
`CODE_ENV_KINDS` const-tuple keeps the runtime list and TS union
locked together.
- Schemas: `metadata.codeEnvRef` and `SkillFile.codeEnvRef` enums
tightened to `['skill', 'agent', 'user']`.
- `primeSkillFiles` writes `kind: 'skill'`, `id: skill._id`,
`version: skill.version`. Cache-hit path reads `codeEnvRef`
directly. Bumping `skill.version` on edit naturally invalidates
the prior cache entry under the new sessionKey.
- `processCodeOutput` writes `kind: 'user'`, `id: req.user.id`. Output
bucket is always user-scoped, regardless of which skill the
execution invoked. New regression test pins the asymmetry.
- `primeFiles` reupload preserves `kind`/`id`/`version?` from the
existing ref so a skill-cache-miss reupload doesn't silently demote
to user bucket.
- `crud.js` upload functions (`uploadCodeEnvFile` /
`batchUploadCodeEnvFiles`) thread `kind`/`id`/`version?` to the
multipart form (codeapi #1455 option α). Without these on the wire,
codeapi falls back to user bucketing and skill-cache invalidation
never fires. Client-side validation mirrors codeapi's validator.
- `Files/process.js` — chat attachments use `kind: 'user'`; agent
setup files use `kind: 'agent'`.
- Drops `entity_id` everywhere (struct, schema sub-docs, write paths,
upload form fields). Drops `'system'` from the kind enum (no emitter
ever existed).
## Test plan
- [x] `cd packages/data-provider && npx jest src/codeEnvRef.spec` — 4 / 4
- [x] `cd packages/data-schemas && npx jest` — 1447 / 1447
- [x] `cd packages/api && npx jest src/agents` — 81 / 81 in skillFiles +
handlers + resources
- [x] `cd api && npx jest server/services/Files server/controllers/agents` —
436 / 436
- [x] `cd api && npx jest server/services/Files/Code` — 98 / 98 (incl.
new "outputs are user-scoped regardless of which skill the execution
invoked" regression and "reupload forwards kind/id/version from
existing ref")
- [x] `npx tsc --noEmit -p packages/data-{provider,schemas}/tsconfig.json
&& npx tsc --noEmit -p packages/api/tsconfig.json` — clean (only
pre-existing unrelated dev errors in storage/balance, untouched here)
## Deploy notes
- **24h cache-miss burst** on first deploy. Inputs (skill caches re-prime
under new sessionKey shape) and outputs (any pre-Phase C skill-output
cached files become unreadable). Bounded by codeapi's 24h TTL.
- **Lockstep with codeapi #1455 and agents #148.** Either repo can land
first since no aliases to drain, but the three deploys must overlap
within the same maintenance window.
- **`@librechat/agents` bump to `3.1.79-dev.0`** required after agents
#148 lands and is published.
## What this enables
Auth bridge work (JWT-based tenant/user identity between LC and codeapi)
— codeapi now derives sessionKey purely from `req.codeApiAuthContext.{
tenantId, userId}`, so the next chapter is replacing the header-asserted
user identity with a verified-claim path.
* 🩹 fix: persist execute_code uploads under codeEnvRef metadata key
Codex review P1 (chatgpt-codex-connector). `Files/process.js` was
storing the upload result under `metadata.fileIdentifier` even though:
- `uploadCodeEnvFile` now returns `{ storage_session_id, file_id }`,
not the legacy magic string.
- The post-cutover schema (`File.metadata.codeEnvRef`) only declares
`codeEnvRef` — mongoose strict mode silently strips unknown keys.
- All readers (`primeFiles`, `getCodeFilesByIds`,
`categorizeFileForToolResources`, controller filtering) check
`metadata.codeEnvRef`.
Net effect of the bug: chat-attached and agent-setup execute_code files
would lose their sandbox reference on save, and primeFiles would skip
them on subsequent code-execution turns — the file blob would still be
available locally but never re-mounted in the sandbox.
Fix: construct the full `CodeEnvRef` (`{ kind, id, storage_session_id,
file_id }`) at the write site and persist under `metadata.codeEnvRef`.
`BaseClient`'s "is this a code-env file" presence check accepts the new
shape alongside the legacy `fileIdentifier` for back-compat with any
pre-cutover records still in the database. Mirrors the same change in
`processAttachments.spec.ts` (which re-implements the BaseClient logic
for testability).
New regression tests in `process.spec.js` cover three cases:
- chat attachments (`messageAttachment=true`) → `kind: 'user'`
- agent setup (`messageAttachment=false`) → `kind: 'agent'`
- legacy `fileIdentifier` key is NOT persisted (would be schema-stripped)
* 🩹 fix: read storage_session_id on primed file refs (Codex P1)
Codex review (chatgpt-codex-connector). After Phase B's per-file
`session_id` → `storage_session_id` rename, `primeFiles` emits the
new field — but `seedCodeFilesIntoSessions` was still reading
`files[0].session_id` for the representative session and `f.session_id`
for the dedupe key. In runs with only primed attachments (no skill
seed), `representativeSessionId` was `undefined`, the function
returned the unchanged map, and `seedCodeFilesIntoSessions` silently
dropped the entire batch. The first `execute_code` call then started
without `_injected_files` and the agent couldn't see prior-turn
artifacts.
Fix:
- `codeFilesSession.ts`: read `f.storage_session_id` for both the
dedupe key and the representative session id. JSDoc updated to
match the new field name.
- `callbacks.js`: the two output-file persistence paths read
`file.session_id` to pass to `processCodeOutput` — switch to
`file.storage_session_id`. The original comment explicitly says
this should be the STORAGE session, which is exactly the field
Phase B renamed.
- `codeFilesSession.spec.ts`: fixture builder uses `storage_session_id`
and `kind: 'user'` to match the post-cutover `CodeEnvFile` shape.
Lockstep coordination: this matches the post-bump shape of
`@librechat/agents` 3.1.79+. CI tsc errors against the currently-pinned
3.1.78 are expected and resolve when the dep bumps in this PR before
merge.
* 📦 chore: Bump `@librechat/agents` to version 3.1.80-dev.0 in package-lock and package.json files
* 🪪 fix: thread kind/id/version through codeapi /download URLs (Phase C α)
Symmetric fix for the upload-side wire change in 537725a. Codeapi's
`sessionAuth` middleware now requires `kind`/`id`/`version?` on every
download/freshness URL — without them it 400s with "kind must be one
of: skill, agent, user" before serving the file.
Three sites construct codeapi-side URLs that go through `sessionAuth`:
- `processCodeOutput` (`Files/Code/process.js`): `/download/<sess>/<id>`
for freshly-generated sandbox outputs. Always `kind: 'user'` +
`id: req.user.id` — code-output files are always user-private,
regardless of which skill the run invoked.
- `getSessionInfo` (`Files/Code/process.js`): `/sessions/<sess>/objects/<id>`
for the 23h freshness check. Pulls kind/id/version straight off the
`codeEnvRef` already in scope — skill files stay skill-bucketed,
user files stay user-bucketed.
- `/code/download/:session_id/:fileId` LC route (`routes/files/files.js`):
proxies to codeapi for manual downloads. Code-output files only on
this route, so `kind: 'user'` + `id: req.user.id`.
The `getCodeOutputDownloadStream` helper in `crud.js` now takes an
`identity` param, validated by a `buildCodeEnvDownloadQuery` helper
that mirrors `appendCodeEnvFileIdentity`'s shape rules: kind required
from the closed `{skill, agent, user}` set, version required for
'skill' and forbidden otherwise. Bad callers fail fast on the client
instead of round-tripping a 400.
Also cleans up two log-noise sources reported alongside the 400:
- `logAxiosError` in `packages/api/src/utils/axios.ts` was dumping
`error.response.data` raw. With `responseType: 'arraybuffer'` that's
a `Buffer` (~4 chars per byte after JSON-serialization); with
`responseType: 'stream'` it's a `Readable` whose internal state
serializes the entire ring buffer + socket. New `renderResponseData`
decodes small buffers as UTF-8 (truncated past 2KB) and stubs streams
as `'[stream]'`. Diagnostics stay useful, log lines stop being
megabytes.
- `/code/download` route's catch was bare `logger.error('...', error)`,
bypassing the redactor. Switched to `logAxiosError` so it benefits
from the same buffer/stream handling.
Tests updated to match the new contract:
- crud.spec: `getCodeOutputDownloadStream` fixtures pass `userIdentity`;
new cases cover skill identity (with version), bad kind rejection,
skill-without-version rejection.
- process.spec: `getSessionInfo` test passes a full `codeEnvRef` object.
* ♻️ refactor: extract codeEnv identity helpers into packages/api
Per the project convention that new backend code lives in TypeScript
under `packages/api`, moves `appendCodeEnvFileIdentity` and
`buildCodeEnvDownloadQuery` from `api/server/services/Files/Code/crud.js`
into a new `packages/api/src/files/code/identity.ts` module.
Both helpers are pure validators that mirror codeapi's
`parseUploadSessionKeyInput` server-side rules (closed kind set,
`version` required for `'skill'` and forbidden otherwise) — they
deserve TS support and a dedicated spec rather than living as
JSDoc-typed helpers in the legacy `/api` workspace. The new module:
- Exports a `CodeEnvIdentity` interface using the
`librechat-data-provider` `CodeEnvKind` discriminated union.
- Adds 13 unit tests in `identity.spec.ts` covering the validation
matrix (skill+version, agent, user, and every rejection path) plus
URL encoding for the download query.
- Re-exported from `packages/api/src/files/code/index.ts` alongside
`classify`, `extract`, and `form`.
Consumer updates:
- `api/server/services/Files/Code/crud.js`: drops the local helpers
and imports them from `@librechat/api`. Net -64 lines.
- `api/server/services/Files/Code/process.js`: same.
- Test mocks for `@librechat/api` in three spec files now stub the
helpers' validation behavior locally rather than pulling them
through `requireActual` (which would drag in provider-config
init-time side effects). The package's `exports` field only
surfaces the root barrel, so leaf imports aren't reachable from
legacy `/api` test setup.
No runtime behavior change. Identity validation rules and emitted
form/query shapes are byte-for-byte identical pre/post.
* 🪪 fix: emit resource_id alongside id on _injected_files (skill 403 fix)
Companion to codeapi #1455 fix and agents 3.1.80-dev.1 — the wire
shape for shared-kind files now requires `resource_id` distinct from
the storage `id`. Without this LC change, codeapi's sessionKey
re-derivation on every shared-kind /exec rejects with 403
session_key_mismatch:
cached: legacy:skill:69dcf561...:v:59 (signed at upload, skill _id)
derived: legacy:skill:ysPwEURuPk-...:v:59 (storage nanoid)
Emit sites updated:
- `primeInvokedSkills` cache-hit path: `resource_id: ref.id` (the
persisted skill `_id` from `codeEnvRef.id`); `id: ref.file_id`
unchanged (storage uuid).
- `primeInvokedSkills` fresh-upload path: `resource_id: skill._id.toString()`
on every primed file (the `allPrimedFiles` builder type now carries
the field).
- `processCodeOutput`'s `pushFile` (Code/process.js): `resource_id: ref.id`
— for `kind: 'user'` this is informational (codeapi derives
sessionKey from auth context) but emitted for shape uniformity
with shared kinds.
Bumps `@librechat/agents` to `^3.1.80-dev.1` (the version that
ships the matching `CodeEnvFile.resource_id` field).
## Test plan
- [x] `cd packages/api && npx jest src/agents` — 67 / 67 pass
(skillFiles fixtures updated to assert `resource_id` on the
emitted CodeSessionContext.files).
- [x] `cd api && npx jest server/services/Files server/controllers/agents` —
445 / 445 pass (process.spec fixtures updated for the reupload
+ cache-hit emission).
- [x] `npx tsc --noEmit -p packages/api/tsconfig.json` — clean.
* fix(skill-tool-call): carry resource_id through primeSkillFiles → artifact
Codeapi was 400ing every /exec following a `handle_skill` tool call
with `resource_id is invalid` (`type: 'undefined'`). Both code paths
in `primeSkillFiles` (cache-hit + fresh-upload) returned files
without `resource_id`/`kind`/`version`, and the artifact in
`handlers.ts` forwarded the stripped shape into
`tc.codeSessionContext.files` → `_injected_files`.
`primeInvokedSkills` (the NL-detected loader) had already been fixed
end-to-end; this commit aligns the tool-invoked path with the same
contract: `resource_id` = `skill._id.toString()`, `kind: 'skill'`,
`version` = the skill's monotonic counter.
Tests added to `skillFiles.spec.ts` lock the contract on
`primeSkillFiles` directly so future refactors can't silently drop
the resource identity again.
* fix(handlers.spec): align session_id → storage_session_id rename + kind discriminator
Pre-existing TS errors against the post-rename `CodeEnvFile` shape:
the test file still used `session_id` on per-file objects (renamed to
`storage_session_id` in agents Phase B/C) and was missing the `kind`
discriminator the discriminated union requires. Both inputs and the
matching `expect.toEqual(...)` mirrors updated together so the
runtime equality check still holds.
Lines 723-732 stay as-is — they sit behind `as unknown as
ToolCallRequest` and TS already skipped them.
* chore: fix `@librechat/agents`, correct version to 3.1.80-dev.0 in package.json files
* chore: bump `@librechat/agents` to version 3.1.80-dev.1 in package.json and package-lock.json
* chore: bump `@librechat/agents` to version 3.1.80-dev.2
* feat(observability): trace file priming chain from primeCodeFiles to _injected_files
Diagnosing the user-upload "files=[] on first /exec" bug requires
seeing where in the LC chain a file ref disappears. Prior to this
patch the chain (primeCodeFiles → primedCodeFiles → initialSessions
→ CodeSessionContext → _injected_files) was opaque end-to-end:
- primeCodeFiles silently dropped files without `metadata.codeEnvRef`
- reuploadFile catches all errors and continues with no signal
- the handlers.ts handoff to codeapi never logged what it was sending
After this patch, a single grep on `[primeCodeFiles]` plus
`[code-env:inject]` shows the full per-file path:
[primeCodeFiles] in: file_ids=N resourceFiles=M
[primeCodeFiles] file=<id> path=skip reason=no-codeenvref filename=...
[primeCodeFiles] file=<id> path=cache-hit-by-session storage_session_id=...
[primeCodeFiles] file=<id> path=reupload reason=no-uploadtime ...
[primeCodeFiles] file=<id> path=reupload reason=stale ...
[primeCodeFiles] file=<id> path=reupload-success oldSession=... newSession=... newFileId=...
[primeCodeFiles] file=<id> path=reupload-failed session=...
[primeCodeFiles] file=<id> path=fresh-active storage_session_id=...
[primeCodeFiles] out: returned=N skippedNoRef=M reuploadFailures=K
[code-env:inject] tool=<name> files=N missingResourceId=K (debug)
[code-env:inject] M/N files missing resource_id ... (warn)
[code-env:inject] tool=<name> _injected_files=0 ... (warn)
The boundary log warns when LC sends zero injected files on a
code-execution tool call — that's the user's actual symptom showing
up at the LC side instead of having to correlate against codeapi's
`Request received { files: [] }`.
Tag chosen as `[code-env:inject]` rather than `[handoff:exec]` to
avoid collision with the app-level "handoff" semantic (subagent
handoff workflow).
Structural cleanup in primeFiles: replaced the `if (ref) { ... }`
nesting with an early `if (!ref) continue` so the per-path
instrumentation hooks land at top-level scope instead of indented
inside a conditional. Behavior unchanged; pushFile / reuploadFile
identical.
Spec fixtures (handlers.spec.ts, codeFilesSession.spec.ts) updated
to include `resource_id` on `CodeEnvFile` literals — required by
the post-3.1.80-dev.2 type now installed.
## Test plan
- [x] `cd packages/api && npx jest src/agents/handlers.spec.ts src/agents/codeFilesSession.spec.ts src/agents/skillFiles.spec.ts` — 69/69 pass
- [x] `cd api && npx jest server/services/Files/Code/process.spec.js` — 84/84 pass
- [x] `npx tsc --noEmit -p packages/api` — clean
- [x] `npx eslint` on all four touched files — clean
* chore: add CONSOLE_JSON_STRING_LENGTH to .env.example for JSON log string length configuration
* fix(files): align codeapi upload filename with LC's sanitized DB filename
User-attached files for code execution were uploading to codeapi
under `file.originalname` (raw upload filename, may contain spaces /
special chars) while LC's DB record stored the sanitized form
(`sanitizeFilename(file.originalname)`, underscores). Codeapi
preserves whatever filename the upload sent, so the sandbox saw
`/mnt/data/<originalname>` while LC's `primeFiles` toolContext text
+ `_injected_files.name` referenced `file.filename` (sanitized).
Visible failure: agent gets system prompt saying
/mnt/data/librechat_code_api_-_active_customer_-_2025-11-05.xlsx
…tries that path, hits `FileNotFoundError`, then notices the
sandbox's actual `Available files` line says
/mnt/data/librechat code api - active customer - 2025-11-05.xlsx
…retries with spaces, succeeds. Wastes a tool call per upload and
leaks raw filenames into model context.
Fix: sanitize once and use the sanitized form in both the codeapi
upload AND the LC DB record. Sandbox path = LC toolContext text =
in-memory ref name. No drift.
Reupload path (`Code/process.js` line 867 `filename: file.filename`)
already uses the sanitized DB name, so it stays consistent with the
fresh-upload path after this change.
## Test plan
- [x] `cd api && npx jest server/services/Files/process` — 32/32 pass
- [x] `npx eslint` on the touched file — clean
* chore: bump `@librechat/agents` to version 3.1.80-dev.3 in package.json and package-lock.json
Required for hanzoai/.github/.github/workflows/docker-build.yml@main —
without it the workflow_call dies as startup_failure with no jobs
dispatched. Caller permissions are a CEILING.
Replaces all ghcr.io/hanzoai/<svc>:latest pins with the latest published
semver tag for each service. Per CLAUDE.md auto-bump policy: mutable
branch tags (:latest, :main, :dev) are deprecated for cluster pins —
only immutable semver permitted.
Bulk update across services with published v* tags. Services without a
published semver remain on :latest until their release pipeline cuts a
v* tag.
* chore: Update axios dependency to version 1.16.0 across multiple package files
* chore: Update express-rate-limit and ip-address dependencies to versions 8.5.1 and 10.2.0 in package-lock.json and package.json
* chore: Update mongoose and hono dependencies to versions 8.23.1 and 4.12.18 across multiple package files
* fix: Add type parameters to mongoose lean queries in accessRole and aclEntry methods
* fix: Add type parameters to mongoose lean queries in action, agent, and agentCategory methods
* chore: Update moduleResolution to 'bundler' in tsconfig.json for api and data-schemas packages
* fix: Update mongoose lean queries to include type parameters across various methods for improved type safety
* 🌐 fix: Percent-encode X-File-Metadata header for Unicode filenames
After #12977 preserved Unicode in filenames, the download route
crashes with ERR_INVALID_CHAR because JSON.stringify(file) now
contains non-ASCII characters that Node.js rejects in HTTP headers
per RFC 7230.
Wrap the header value in encodeURIComponent on the server and
decodeURIComponent on the client before JSON.parse.
* fix: Update file route tests after dev merge
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* feat(ui): add message navigation strip and redesign scroll-to-bottom button
Add a floating vertical navigation strip on the right edge of the chat
area that lets users jump between messages quickly. Each message gets an
indicator line (wider for assistant, narrower for user) with HoverCard
previews showing truncated message text. IntersectionObserver tracks
which messages are currently visible and highlights their indicators.
Redesign the scroll-to-bottom button: solid backgrounds instead of
semi-transparent, clean enter/exit animations without twist/rotate,
no hover float animation, positioned at the right edge of the chat
form instead of center.
* fix(ui): prevent message nav layout shift on scroll
Use a fixed-height container for each indicator so the nav strip
maintains consistent dimensions when indicators transition between
active and inactive states.
* fix(ui): debounce message nav refresh and persist visibility state
Debounce entry refresh (200ms) to avoid thrashing from rapid DOM
mutations during code block rendering. Persist the visible message
set across IntersectionObserver reconnections to prevent momentary
empty state that disabled navigation buttons.
* fix(ui): prevent nav buttons from disabling during fast scroll
- Fall back to last known active index when IntersectionObserver
reports no visible messages during rapid scrolling
- Lower intersection threshold from 10% to 1% for long messages
- Fix preview text to skip the message header (Prompt N: username)
* fix(ui): scroll to message start when using nav arrow buttons
Arrow buttons now use block: 'start' to always scroll to the top of
the target message. Indicator dots keep block: 'nearest' for minimal
repositioning on direct clicks.
* fix(ui): account for header offset when scrolling to messages
Use manual scrollTo with a 56px offset to prevent the fixed header
from covering the top of the target message when using arrow buttons.
* fix(ui): improve message nav scrolling and visual subtlety
- Up button scrolls to current message top first before jumping to
previous, preventing skipped messages on long content
- Down button consistently scrolls to the start of the next message
- Nav strip is faded (opacity 30%) by default, fully visible on hover
- Background, buttons, and indicators all appear on hover of the
nav area using group hover coordination
* fix(ui): use native scroll-margin-top for reliable message navigation
Replace manual scrollTo calculations with scrollIntoView + CSS
scroll-margin-top on .message-render elements. The browser handles
scroll offset natively, eliminating positioning errors during smooth
scroll animations.
* fix(ui): use firstActiveIndex for both nav directions
Use firstActiveIndex (topmost visible message) for both up and down
navigation. Down now advances one message at a time from what the user
is currently reading instead of jumping past all visible messages.
Remove unused lastActiveIndex.
* fix(ui): address PR review feedback
- Scope getMessageEntries query to scroll container instead of document
- Include preview text in entries equality check to catch content
updates during streaming/edits
- Move scroll button transition to base state so release animates
smoothly instead of snapping back
* fix(ui): make message nav scroll precise and chevrons reliable
- Bump .message-render scroll-margin-top from 1rem to 4rem so messages
land below the 52px absolute gradient header instead of behind it.
- Drive chevron jumps from live scrollTop + offsetTop comparison rather
than the IntersectionObserver-derived firstActiveIndex, which lagged
behind rapid clicks and treated any 1px-visible message as "current".
- Track canGoUp / canGoDown from the same scroll-position comparison so
the disabled state matches what the buttons will actually do.
- Auto-center the indicator column on the visible message range and
smooth-scroll it via rAF so 500+ indicators stay at 60fps.
- Pull entry data from useGetMessagesByConvoId (with a DOM fallback) so
previews are state-backed instead of scraped from rendered markup.
- Memoize MessageIndicator and filter MutationObserver to .message-render
add/remove only.
- Add 5 i18n keys (com_ui_message_nav*) for nav and indicator labels.
* perf(ui): skip off-screen message layout and fix resulting scroll drift
Large conversations used to freeze the main thread during sidebar
toggles because every animated frame had to relayout every message.
With ~3000 message elements on this branch: avg frame 650ms,
max 1701ms (~1.5fps) during the 300ms transition. Adding
`content-visibility: auto` with `contain-intrinsic-size: auto 200px`
on .message-render lets the browser skip layout/paint for messages
outside the viewport, dropping avg frame to 33ms and max to 74ms
(~30fps, feels responsive).
content-visibility comes with a trade-off though: off-screen messages
use the 200px intrinsic-size estimate until they're measured. That
broke indicator-click scrolling on long conversations, landing 1-2
messages off the target because scrollIntoView computed its target
scrollTop once with stale estimates, and intermediate messages
shrunk/grew as they rendered during the smooth scroll.
Replaced scrollIntoView with a manual rAF scroll that re-reads the
target's getBoundingClientRect every frame and eases toward the
*current* target. Verified drift=0 across fake-0, fake-50, fake-250,
fake-450 (messages near the bottom naturally land higher than
scroll-margin when the container is already at max scroll — expected).
Also two small MessageNav.tsx hot-path cleanups:
- Use col.children[i] instead of col.querySelector by data-msg-id for
the indicator-column centering lookup (entries map 1:1 to column
children since HoverCardTrigger asChild forwards to the button).
- Compare visibility set contents before setActiveIds, so an
IntersectionObserver flush with unchanged membership doesn't force
a re-render and 500x memo comparisons.
* revert(ui): drop content-visibility on .message-render
Didn't deliver the expected sidebar-toggle perf win in real-world
usage, and its intrinsic-size estimation introduced the exact kind of
scroll drift we then had to work around. The rAF scroll in MessageNav
is orthogonal to this and stays — it works fine with or without
content-visibility.
* fix(ui): address PR review — a11y, tests, and MessageNav correctness
- ScrollToBottom aria-label now runs through useLocalize instead of being
hardcoded English. Added com_ui_scroll_to_bottom translation key.
- MessageNav nav expands on keyboard focus-within, not just pointer hover.
- Indicator buttons expose aria-current="true" for the active message and
get a visible focus-visible ring. Chevron buttons get the same ring so
keyboard users can see focus.
- Cancel in-flight rAF scrolls when a new navigation starts, so clicking
a second indicator mid-animation doesn't race the first loop on
container.scrollTop.
- Invalidate the cached offsetsTop/offsetsBottom arrays via a
ResizeObserver on the scroll content. Previously heights that changed
after mount (code blocks rendering, images loading) left canGoUp /
canGoDown and the indicator-column centering reading stale positions.
- Observe IntersectionObserver entries incrementally. The observer is
now created once per scroll container and entries add/remove on
change instead of the whole observer being torn down and rebuilt for
every new message.
- memo() the default export so parent re-renders don't cascade through
MessageNav when entries/activeIds haven't changed.
- Add 18-test suite covering rendering threshold, user/assistant
indicator styling, preview sourcing (React Query vs DOM fallback vs
truncation), accessibility (aria-label, aria-current, chevron
disabled state), click-driven rAF scroll + cancellation, and observer
lifecycle (observe on mount, incremental sync, unobserve on removal,
disconnect on unmount).
* fix(ui): catch in-place message id mutations and react to layout shifts
Follow-ups from deep review:
- MutationObserver on .message-render now also watches the id attribute.
During the SSE lifecycle a single DOM node's id cycles through three
values (client UUID -> createdHandler id -> server id, see the comment
in MultiMessage.tsx), which meant the previous childList-only observer
never refreshed entries after a streaming response. Nav clicks on the
most recent message were silently failing because getElementById
returned null for the stale id.
- ResizeObserver now calls scheduleTick() instead of only flipping a
flag. The flag was only consumed inside the scroll handler's tick, so
heights that changed while the user wasn't scrolling (assistant message
streaming in, code blocks highlighting) left offsetsTop/offsetsBottom
stale and canGoUp / canGoDown wrong. Both handlers now route through
scheduleTick so a resize and a scroll share the same rAF slot.
- Unify scroll and resize callbacks on scheduleTick. Removes a duplicate
rAF path and makes the effect cleaner.
- Single-pass build of newIds during incremental IO sync (previously
entries.map().new Set() did two passes for no reason).
- CSSTransition timeouts drop from 550/700 to 300/250 to match the new
scroll-to-bottom animations. Old values left the button in the DOM
for up to 450ms after the exit animation finished.
- ScrollToBottom.tsx imports reordered to longest-first per project
convention.
- style.css: collapse split `border: 1px solid` + `border-color` into
one shorthand; dark variant still overrides border-color cleanly.
- Tests: add SSE-lifecycle test that mutates a .message-render id in
place and asserts the nav now shows an indicator for the new id and
none for the old one. HoverCard mock no longer spreads unknown props
to the DOM div (drops a React warning).
* fix(ui): address deep-review follow-ups on MessageNav
- Move activeScrollToken from module scope to a per-instance useRef
(scrollTokenRef). When LibreChat eventually mounts more than one
MessageNav side-by-side (multi-panel / added-convo view) a click in
one panel will no longer cancel an in-flight smooth scroll in another.
scrollToMessageStart is now an instance useCallback and the button
click path goes through an onSelect prop on MessageIndicator, keeping
the memoized indicator stable.
- messagesById goes through a ref (messagesByIdRef) so refreshEntries is
no longer recreated on every streaming token. Previously messagesById
landed in both the useMemo and the refreshEntries dep array, so each
streaming response rebuilt the MutationObserver effect dozens of times
per second. A separate small effect still calls refreshEntries when
messagesById changes, so previews stay fresh.
- Extract USER_TURN_SELECTOR constant and tighten the text-preview type
narrowing so we no longer need the `as { value?: string }` cast (TS
narrows string | TextData correctly through the `typeof object` +
property access guard).
- Cache the computed scroll margin (4rem = 64px) in scrollMarginRef so
the nav callbacks don't call getComputedStyle on every click.
- Tests: add a two-instance isolation test that verifies scroll tokens
don't cross between mounted MessageNavs. Drop the unused `import React
from 'react'` pattern in favor of local type aliases.
- client/package.json: bump @babel/preset-typescript to ^7.28.5. The old
^7.22.15 constraint was resolving to 7.23.3 via hoisting, which can't
parse modern `import type` syntax on a clean install and was breaking
the test suite.
* fix(ui): address re-review — clean lockfile + ScrollToBottom ref target
- package-lock.json: the preset-typescript bump last commit pulled in
transitive Babel packages resolved through a local internal registry
(npm.internal.berry13.com). Rewrote those 31 entries back to the
public npmjs.org registry so CI and contributors can install cleanly.
Integrity hashes unchanged — content-addressed.
- ScrollToBottom now forwards its ref to the wrapping <div> instead of
the inner <button>. CSSTransition's nodeRef + unmountOnExit can now
add transition classes to the actual root element, so the layout
wrapper is what mounts/unmounts, not just the button. Updated
scrollToBottomRef type in MessagesView to HTMLDivElement.
- jumpToPrevious / jumpToNext skip the document.getElementById fallback
lookup when scrollMarginRef is already populated, which is the normal
case after the first scroll-tick effect run.
* fix(ui): preserve IntersectionObserver across in-place id mutations
The IO sync effect was observing new ids before unobserving old ones.
During the SSE lifecycle of a fresh chat, a single .message-render node
cycles through three ids (client UUID -> handler id -> server id). When
the id mutated on the same element, the effect would call observe(el)
then unobserve(el) on that element in the same pass — leaving it
permanently unobserved. The active-message highlight never updated for
the new id until a hard refresh rebuilt everything from scratch.
Switched to element-identity tracking. Build an element -> newId map
from entries, then for each currently observed [oldId, el]:
- if the element no longer appears in entries, unobserve and drop it
- if the element appears under a new id, migrate observed and
visibleSet keys in place — the IntersectionObserver keeps watching
the same DOM node uninterrupted
Genuinely new elements get observed afterward as before. Rename doesn't
fire an IO callback, so flush activeIds manually when at least one
migration happened. Existing convos already had this working because
their ids never mutate after load — only fresh chats hit the SSE id
cycle, which matches the reproduction.
* fix(ui): keep message nav current and pinned at bottom
* 🗂️ feat: add `status` lifecycle to file records for two-phase previews
Schema and model foundation for decoupling the agent's final response
from CPU-heavy office-format HTML extraction.
- `MongoFile.status: 'pending' | 'ready' | 'failed'` (indexed) and
`previewError?: string` mirror the lifecycle: phase-1 emits the file
record at `pending` so the response is unblocked; phase-2 transitions
to `ready` (with text/textFormat) or `failed` (with previewError) in
the background. Absent for legacy records — clients treat that as
`ready` for back-compat.
- Mirror types added to `TFile` in data-provider so frontend cache
consumers see the new fields.
- New `sweepOrphanedPreviews(maxAgeMs)` method on the file model
recovers stale `pending` records left behind by a process restart
mid-extraction; transitions them to `failed` with
`previewError: 'orphaned'`. Cheap because `status` is indexed.
* ⚡ feat: two-phase code-execution preview flow (unblocks final response)
The agent's final response no longer waits on CPU-heavy office HTML
extraction. Phase-1 (download + storage save + DB record at
`status: 'pending'`) is awaited as before; phase-2 (extract +
`updateFile`) runs in the background with a hard 60s ceiling.
Three flows, all funneling through `processCodeOutput` and updated to
the new `{ file, finalize? }` return shape:
- `callbacks.js` (chat-completions + Open Responses streaming): emit
the phase-1 attachment immediately (carries `status: 'pending'` for
office buckets so the UI shows "preparing preview…"), then
fire-and-forget `finalize()`. If the SSE stream is still open when
phase-2 lands, push an `attachment` update event with the same
`file_id` so the client merges over the placeholder in place.
- `tools.js` direct endpoint: same split — return the phase-1
metadata immediately, run extraction in the background. Client
polls for the resolved record.
`finalize()` wraps the existing 12s per-render timeout in a 60s outer
`withTimeout`. The HTML-or-null contract from #12934 is preserved:
office types that fail extraction transition to `status: 'failed'`
with `previewError: 'parser-error' | 'timeout'` rather than falling
back to plain text (would be an XSS vector).
Promises continue running after the HTTP response closes (Node
doesn't kill them). The boot-time orphan sweep covers the only case
that loses progress — actual process restart mid-extraction.
`primeFiles` annotates the agent's `toolContext` line for prior-turn
files: `(preview not yet generated)` for pending, `(preview
unavailable: <reason>)` for failed. The model can volunteer "you can
still download it" instead of pretending the preview is fine.
`hasOfficeHtmlPath` exported from `@librechat/api` so `processCodeOutput`
can decide whether a file expects a preview at all.
* 🔍 feat: `GET /api/files/:file_id/preview` endpoint and boot orphan sweep
- New `GET /api/files/:file_id/preview` route returns
`{ status, text?, textFormat?, previewError? }`. The frontend's
`useFilePreview` React Query hook polls this while phase-2 is in
flight, then auto-stops on terminal status. ACL identical to the
download route (reuses `fileAccess` middleware). Defaults `status`
to `'ready'` for legacy records so back-compat is implicit.
`text` only included when `status === 'ready'` and non-null —
preserves the HTML-or-null security contract from #12934.
- `sweepOrphanedPreviews()` invoked on boot in both `server/index.js`
and `server/experimental.js`. Recovers any `pending` records left
behind by a process restart mid-extraction (the only case the
in-process two-phase flow can't handle on its own). Fire-and-forget
so a transient sweep failure doesn't block startup.
* 🖥️ feat: frontend two-phase preview consumer (polling + UI states)
Wires the React side to the new lifecycle so the user sees what's
happening with their file while phase-2 extraction runs in the
background and after the response stream closes.
- `useAttachmentHandler` upserts by `file_id` (was append-only) so
the phase-2 SSE update event merges over the pending placeholder
in place. Lightweight attachments without a `file_id`
(web_search / file_search citations) keep the legacy append path.
- `useFilePreview(file_id)` React Query hook with
`refetchInterval: (data) => data?.status === 'pending' ? 2500 : false`
so polling auto-stops on the first terminal response without the
caller having to flip `enabled`.
- `useAttachmentPreviewSync(attachment)` bridges polled data into
`messageAttachmentsMap`. Polling enabled iff
`status === 'pending' && isAnySubmitting` — per the design ask:
active polling while the LLM is still generating, then quiet.
Process-restart and post-stream cases are covered by polling on
the next interaction.
- `Attachment.tsx` renders a small `PreviewStatusIndicator` (spinner +
"Preparing preview…" for pending, alert icon + "Preview unavailable"
for failed) inside `FileAttachment`. Download button stays fully
functional in both states. Two new English locale keys.
- Data-provider scaffolding: `TFilePreview` type, `endpoints.filePreview`,
`dataService.getFilePreview`, `QueryKeys.filePreview`.
* 🧪 fix: stub `useAttachmentPreviewSync` in pre-existing Attachment test mocks
The new `useAttachmentPreviewSync` hook is called unconditionally inside
`FileAttachment` (added in the prior commit). Two pre-existing test
files mock `~/hooks` to provide `useLocalize` only — the un-mocked
preview hook reference resolved to undefined and crashed render with
`(0 , _hooks.useAttachmentPreviewSync) is not a function` on the
Ubuntu/Windows CI runners.
Fix is local to the test mocks: add a no-op stub that returns
`{ status: 'ready' }` so the component renders the legacy chip path.
The two-phase preview behavior itself has its own dedicated suites
(`useAttachmentHandler.spec.tsx`, `useAttachmentPreviewSync.spec.tsx`).
* 🐛 fix: route phase-2 attachment update to current-run messageId
Codex P1 review on PR #12957. `processCodeOutput` intentionally
preserves the original DB `messageId` across cross-turn filename reuse
so `getCodeGeneratedFiles` can still trace a file back to the
assistant message that originally produced it. The phase-1 SSE emit
already routes by the current run's messageId — `processCodeOutput`
runtime-overlays it via `Object.assign(file, { messageId, toolCallId })`
and the callback writes `result.file` directly.
Phase-2 was passing the raw `updateFile` return through
`attachmentFromFileMetadata`, which read `messageId` straight off the
DB record. On a turn-N run that re-emitted a filename from turn-1
(e.g. agent writes `output.csv` again), the phase-2 SSE update
routed to `turn-1-msg` instead of `turn-N-msg`. Frontend's
`useAttachmentHandler` upserts under the wrong messageAttachmentsMap
slot — turn-N's pending chip stays stuck at "preparing preview…"
while turn-1's already-resolved attachment gets re-merged.
Fix: thread `runtimeMessageId` through `attachmentFromFileMetadata`
and pass `metadata.run_id` from the phase-2 emit site. Mirrors how
phase-1 sources its messageId. Tests cover the cross-turn reuse case
plus the writableEnded / null-finalize / no-finalize paths to lock
in the broader phase-2 emit contract.
* 🛠️ refactor: address codex audit findings (wire-shape parity, DRY, defensive catch)
Comprehensive audit on PR #12957. Resolves all valid findings:
- **MAJOR #1 — Wire-shape parity**: phase-1 ships the full `fileMetadata`
record over SSE; phase-2 was using a tight `attachmentFromFileMetadata`
projection. Drop the projection and have phase-2 spread `{...updated,
messageId, toolCallId}` so both events match the long-standing
legacy phase-1 shape clients depend on.
- **MAJOR #2 — DRY**: extract `runPhase2Finalize({ finalize, fileId,
onResolved })` into `process.js` (alongside `processCodeOutput` whose
contract it pairs with). Both `callbacks.js` paths and `tools.js`
now flow through it. Single catch path eliminates divergence
surface — the fix landed in 01704d4f0 (cross-turn messageId routing)
was a symptom of this duplication risk.
- **MINOR #3 — JSDoc accuracy**: `finalizePreview`'s buffer is bounded
by `fileSizeLimit`, not the 1MB extractor cap. Updated and added a
note about peak heap from queued buffers.
- **MINOR #4 — Defensive catch**: `runPhase2Finalize`'s catch attempts
a best-effort `updateFile({ status: 'failed', previewError:
'unexpected' })` for the file_id, so a programming bug in
`finalizePreview` doesn't leave the record stuck `'pending'` until
the next boot-time orphan sweep.
- **NIT #6 — Stale PR refs**: 12952 → 12957 in 3 places.
- **NIT #7 — Schema bound**: `previewError` capped at `maxlength: 200`
to prevent a future codepath from accidentally persisting a stack
trace.
Skipped per audit verdict (non-blocking):
- #5 (memory pressure): documented in JSDoc; impl change was reviewer's
"consider", not actionable.
- #8 (double DB query per poll): low cost, indexed by_id, polling is
gated narrow.
- #9 (TAttachment cast): the union type is intentional; the casts are
safe widening, refactoring TAttachment is invasive and out of scope.
Tests: 11 new (7 `runPhase2Finalize` unit tests covering happy path,
null-finalize, throws, double-fail, no-fileId, no-onResolved; +4
wire-shape parity assertions in the existing cross-turn test). 328
backend tests pass; 528 frontend tests pass; lint and typecheck clean.
* 🛡️ refactor: address codex P1+P2 + rename to drop phase-1/2 jargon
Codex round 2 review on PR #12957 caught two race conditions and one
recovery gap, all triggered by cross-turn filename reuse (`claimCodeFile`
intentionally returns the same `file_id` for the same
`(filename, conversationId)` across turns). Plus naming cleanup the
user requested — internal "phase 1 / phase 2" vocabulary leaks across
sprints, replace it everywhere with terms describing what's actually
happening.
P1 — stale render overwrites newer revision (process.js)
Two turns reusing `output.csv` share a `file_id`. If turn-1's
background render resolves AFTER turn-2's persist step, the
unconditional `updateFile` writes turn-1's stale text/status over
turn-2's pending placeholder. Fix: stamp a fresh `previewRevision`
UUID on every emit, thread it through `finalizePreview`, and make
the commit conditional via a new optional `extraFilter` argument
on `updateFile` (`{ previewRevision: <expected> }`). The defensive
`updateFile` in `runPreviewFinalize`'s catch uses the same guard
so a programming error from an older render also can't override a
newer turn.
P1 — stale React Query cache on pending remount (queries.ts)
Same root cause from the frontend side. Cache key
`[QueryKeys.filePreview, file_id]` may hold a prior turn's `'ready'`
payload; with `refetchOnMount: false` and the polling gate on
`pending`, polling never starts for the new placeholder. Fix:
`useAttachmentHandler` invalidates that query whenever an attachment
with a `file_id` arrives. Both initial-emit and update events
trigger invalidation — uniform gate.
P2 — quick-restart orphans skipped by boot sweep (files.js)
Boot `sweepOrphanedPreviews` uses a 5-min cutoff for multi-instance
safety. A crash + restart inside the cutoff leaves `pending` records
that never get touched again. Fix: lazy sweep inside the preview
endpoint — if a polled record is `pending` and `updatedAt` is older
than 5 min, mark it `failed:orphaned` on the spot before responding.
Conditional on the same `updatedAt` we observed so a concurrent
legitimate update wins. Cheap, bounded by user activity.
Naming cleanup
- `runPhase2Finalize` → `runPreviewFinalize`
- `PHASE_TWO_TIMEOUT_MS` → `PREVIEW_FINALIZE_TIMEOUT_MS`
- All `phase-1` / `phase-2` / `two-phase` prose replaced with
"the immediate emit", "the deferred render", "the persist step",
"the deferred preview", etc. Skill-feature `phase 1/2` references
(different feature) left alone.
Tests: 10 new (4 lazy-sweep × preview endpoint, 3 cache-invalidation ×
useAttachmentHandler, 3 extraFilter × updateFile data-schemas).
Backend 332/332, frontend 531/531, data-schemas 37/37, lint clean.
* 🛠️ refactor: address comprehensive review (round 3) — stale-cache MAJOR + 3 minors
Comprehensive review on PR #12957 caught a P1 follow-on bug from the
prior `invalidateQueries` fix, plus 3 maintainability findings.
MAJOR: stale React Query cache not actually fixed by `invalidateQueries`
The previous fix called `invalidateQueries` to flush stale cached
preview data on cross-turn filename reuse. But `useFilePreview` had
`refetchOnMount: false`, which made the new observer read the
stale-marked 'ready' data without refetching. The polling
`refetchInterval` then evaluated against stale 'ready' → returned
`false` → polling never started → user stuck on stale content.
Fix (belt-and-suspenders):
a) `useAttachmentHandler` switched to `removeQueries` — drops the
cache entry entirely so the next mount has nothing to read and
must fetch.
b) `useFilePreview` no longer sets `refetchOnMount: false`, so the
React Query default (`true`) kicks in — second line of defense
if any future codepath observes stale data before the handler
has a chance to evict.
MINOR: `finalizePreview` JSDoc missing `previewRevision` param
Added with explanation of the conditional update guard.
MINOR: asymmetric stream-writable guard between SSE protocols
Chat-completions delegated the gate to `writeAttachmentUpdate`;
Open Responses inlined `!res.writableEnded && res.headersSent`.
Extracted `isStreamWritable(res, streamId)` predicate; both paths
+ `writeAttachmentUpdate` now share the single source of truth.
NIT: `(data as Partial<TFile>).file_id` cast repeated 4 times
Extracted to a `fileId` local at the top of the handler.
Tests: existing 9 invalidate-tests rewritten as remove-tests; +1 new
lock-in test asserts removeQueries is called and invalidateQueries
is NOT (regression guard against round-3 finding). 332 backend pass,
532 frontend pass, lint clean.
Skipped findings (deferred / acceptable):
- MINOR: post-submission pending state has no auto-recovery — the
`isAnySubmitting` polling gate was the user's explicit design;
LLM context surfaces failed/pending so the model can volunteer.
Worth a follow-up if real users hit it.
- NIT: double DB query per preview poll — reviewer marked acceptable;
changing `fileAccess` middleware is out of scope.
* 🛡️ test: address comprehensive review NITs (initial-emit guard + isStreamWritable coverage)
NIT — chat-completions initial emit skips writableEnded check
The Open Responses initial emit was switched to use the new
`isStreamWritable` predicate in the round-3 commit, but the
chat-completions initial emit kept the older narrower check
(`streamId || res.headersSent`). On a client disconnect mid-stream
(`writableEnded === true`) it would still hit `res.write` and
raise `ERR_STREAM_WRITE_AFTER_END` — caught by the outer IIFE
catch but logged as noise. Switch this site to `isStreamWritable`
too so both initial-emit paths share the same gate as the
deferred update emits.
NIT — `isStreamWritable` not directly unit-tested
The predicate was only covered indirectly via the deferred-preview
SSE tests (writableEnded skip, headersSent check). Export from
`callbacks.js` and add 5 parametric tests pinning down each branch
(streamId truthy, res null, !headersSent, writableEnded, happy
path) so a future condition addition can't silently regress.
* 🐛 fix: stuck "Preparing preview…" + inline the chip subtitle
Two related fixes for a stuck-spinner bug a user reported in manual
testing of PR #12957.
**Stuck spinner (the bug)**
The deferred preview render can complete a few seconds AFTER the SSE
stream closes (typical case: PPTX render finishes ~3s after the LLM
emits FINAL). When that happens, the SSE update is silently dropped
(`isStreamWritable` returns false on a closed stream) and polling is
the only recovery path.
The earlier polling gate was `status === 'pending' && isAnySubmitting`,
which mirrored the original design intent ("only query while the LLM
is still generating"). But `isAnySubmitting` flips false the moment
the model emits FINAL — milliseconds before the deferred render
commits. Polling never runs, the chip stays "Preparing preview…"
forever even though the DB has `status: 'ready'` with valid HTML.
Drop the `isAnySubmitting` part of the gate. `useFilePreview`'s
`refetchInterval` is already a function-form that returns `false` on
the first terminal response, so polling auto-stops within one tick of
resolution. The server-side render ceiling (60s) plus the lazy sweep
in the preview endpoint cap the worst case to ~24 polls per pending
attachment. Polling itself never blocks UX — the gate's purpose was
"don't waste cycles", and capping by terminal status is the correct
expression of that.
**Inline the chip subtitle (the visual)**
The previous design rendered "Preparing preview…" as a loose-feeling
spinner+text BELOW the file chip. The chip itself looked done while a
floating annotation said it wasn't.
`FileContainer` gains an optional `subtitle?: ReactNode` prop that
overrides the default file-type label. `Attachment.tsx` passes a
`PreviewStatusSubtitle` (spinner + "Preparing preview…" / alert +
"Preview unavailable") into that slot when the file's preview is
pending or failed. The chip footprint stays identical to its `'ready'`
form — just the second row swaps from "PowerPoint Presentation" to
the status indicator. No floating element, no layout shift.
Tests: regression test pinning down "polling stays enabled after the
LLM finishes" so a future revert can't reintroduce the stuck-spinner
bug. Existing FileContainer tests pass unchanged (subtitle override
is opt-in). 522 frontend tests pass; lint clean.
* 🐛 fix: deferred-preview survives reload + matches artifact card chrome
Fixes the remaining stuck-pending case after the polling gate fix: on
a reloaded conversation, message.attachments come from the DB frozen at
the immediate-persist `status: 'pending'`, but `messageAttachmentsMap`
is empty because no SSE handler ever fired for that messageId. Polling
now INSERTS a new live entry when no record matches the file_id, and
`useAttachments` merges live entries onto DB entries by file_id so the
resolved text/textFormat reach `artifactTypeForAttachment` and the
chip routes through the proper PanelArtifact card.
Also replaces the small file chip used during the pending state with
a PreviewPlaceholderCard that mirrors ToolArtifactCard chrome, so the
transition to the resolved PanelArtifact no longer reshapes the UI.
* ✨ feat: auto-open panel when deferred preview resolves pending→ready
The legacy auto-open path is gated only on `isSubmitting`, so an
office-file preview that resolves *after* the SSE stream closes would
render in place but never auto-open the panel — even though that's
exactly the moment the result becomes meaningful to the user. Adds a
per-file_id one-shot signal that `useAttachmentPreviewSync` flips on
the pending→ready edge; `ToolArtifactCard` consumes it on mount and
auto-opens regardless of submission state. The signal is *only* set on
the actual transition (history loads of pre-resolved files don't
trigger it) and is consumed once (panel close + reopen on the same
card stays user-controlled).
* 🐛 fix: drop placeholder Terminal overlay + scope auto-open to fresh resolutions
Two fixes for issues spotted in manual testing of the deferred-preview
auto-open feature:
1. PreviewPlaceholderCard was passing `file={attachment}` to FilePreview,
which triggered SourceIcon's Terminal overlay (`metadata.fileIdentifier`
is set on every code-execution file). The artifact card itself doesn't
show that overlay; the placeholder shouldn't either, so the
pending→resolved transition is visually seamless.
2. The `previewJustResolved` flag flipped on every pending→ready
transition observed by the polling hook — including stale-pending
DB records that resolve via the first poll on a *history load*.
Conversations whose immediate-persist snapshot left attachments at
`status: 'pending'` would yank the panel open every revisit.
Adds `mountedDuringStreamRef` to the hook (mirroring ToolArtifactCard)
so the flag fires only when the hook itself was mounted during an
active turn — preserving the pre-PR contract that the panel only
auto-opens for results the user is actively waiting on, never for
history.
* 🐛 fix: don't downgrade preview to failed when only the SSE emit throws
Codex P2 finding on PR #12957: the original chain placed `.catch` after
`.then(onResolved)`, so a throw inside `onResolved` (transport-side
errors — SSE write race after stream close, an emitter listener
throwing) would propagate into the finalize catch and persist
`status: 'failed'` / `previewError: 'unexpected'`. That surfaced
"preview unavailable" in the UI for a perfectly valid file, and
degraded next-turn LLM context to reflect a non-existent failure.
Wraps `onResolved` in its own try/catch so emit errors are logged but
do not affect the file's persisted status. Extraction success and
emit success are now independent: if extraction succeeds and
`finalizePreview` writes the terminal status, the polling layer / next
page load surfaces the resolved preview even if this turn's SSE emit
didn't land.
* 🛡️ fix: run boot-time orphan sweep under system tenant context
Codex P2 finding on PR #12957: `File` is tenant-isolated, so under
`TENANT_ISOLATION_STRICT=true` the boot-time `sweepOrphanedPreviews`
threw `[TenantIsolation] Query attempted without tenant context in
strict mode` and the recovery path silently failed every restart.
Stale `status: 'pending'` records would be stuck until a user happened
to poll the preview endpoint and trigger the lazy sweep — which only
covers the file the user is currently looking at, not the bulk
candidate set the boot sweep is designed to recover.
Wraps the sweep in `runAsSystem(...)` in both boot paths
(`api/server/index.js` and `api/server/experimental.js`) and pins the
contract with regression tests in `file.spec.ts` — one test asserts
the bare call throws under strict mode, the other asserts the
`runAsSystem`-wrapped call succeeds.
* 🧹 chore: trim verbose comments from previous commit
* 🧹 chore: address review findings (dead branch, lazy-sweep cutoff, stale JSDoc)
- finalizePreview: drop unreachable !isOfficeBucket branch (caller
already gates on hasOfficeHtmlPath, so this path is always office)
- preview endpoint: drop lazy-sweep cutoff from 5min to 2min — anything
past the 60s render ceiling is definitively orphaned, and per-request
sweep can be tighter than the per-instance boot sweep
- strip stale `isSubmitting` references from JSDoc in 3 spots (the
client-side gate was removed in 9a65840)
Skipped: function-length (#3) and client-side polling cap (#4) —
refactors without correctness/perf wins; remaining NITs.
* 🧹 fix: trim 1 query off pending polls + clear stale lifecycle on cross-shape updates
- Preview endpoint: reuse fileAccess middleware's record for the
lifecycle check; only re-fetch with text on the terminal ready
response. Cuts the typical poll lifecycle from 2(N+1) to N+1
queries, since the vast majority of polls hit while pending and
don't need text at all.
- processCodeOutput non-office branch: explicitly null out status,
previewError, previewRevision (codex P2). Without this, an update at
the same (filename, conversationId) where the prior emit was an
office file leaves stale lifecycle fields and the client renders
the wrong state for the now non-office artifact.
- Tests: rewire preview.spec mocks for the new shape, add boundary
test pinning the 2min cutoff, add regression test for the
cross-shape update.
* 🐛 fix: keep polling on transient errors but cap permanently-broken endpoint
Codex P2: the previous `data?.status === 'pending' ? 2500 : false` gate
killed polling on the first transient error. With `retry: false`, a 500
left `data` undefined, the callback returned false, and the chip was
stuck "Preparing preview…" forever — exactly the bug the polling layer
was supposed to recover from.
Inverts the gate: stop on terminal success (`ready`/`failed`) or after
5 consecutive errors. Transient errors keep retrying; a permanently
broken endpoint caps at ~12.5s instead of polling forever. Predicate
extracted as `previewRefetchInterval` for direct unit testing without
fighting React Query's timer machinery.
* ✨ feat: render pending-preview files in their own row
Pending deferred-preview chips now bucket into a separate row above
the resolved attachments — reads as "this is still happening" rather
than mixing with completed downloads. Once status flips to ready, the
chip re-buckets into panelArtifacts; failed re-buckets into the file
row alongside other downloads.
* 🎨 fix: render pending-preview chips in the panel-artifact row, not the file row
Previous bucketing put pending chips in the file row (since
`artifactTypeForAttachment` returns null for empty-text records). The
pending placeholder is a future panel artifact — sharing the row keeps
the chip in place when it resolves instead of jumping rows.
Plain files still get their own row.
* 🐛 fix: phase-1 SSE replay must not regress a resolved attachment
Codex P1: useEventHandlers.finalHandler iterates
responseMessage.attachments at stream end and dispatches each through
the attachment handler. Those records are the immediate-persist
snapshot (status:pending, text:null) — if a deferred update has
already moved the same file_id to ready/failed, the existing merge
let the pending fields win and downgraded the resolved record. Result:
chip flickers back to pending and polling restarts until the lazy
sweep corrects.
Pin the terminal lifecycle fields (status, text, textFormat,
previewError) when existing is ready/failed and incoming is pending.
Other field updates still go through.
* 🐛 fix: track preview-poll error cap outside React Query state
Codex P2: the previous cap relied on `query.state.fetchFailureCount`,
but React Query v4's reducer resets that to 0 on every fetch dispatch
(the `'fetch'` action). With `retry: false`, each failed poll left
count at 1 and the next dispatch reset it back to 0, so the `>= 5`
branch never fired and a permanently-broken endpoint polled forever.
Track consecutive errors in a module-level Map keyed by file_id,
incremented in a thin `fetchFilePreview` wrapper around the data
service call. The Map is cleared on success and on cap-stop, so
memory is bounded by in-flight pending file_ids per session.
* 📦 chore: bump `@librechat/agents` to v3.1.78
v3.1.78 ships [danny-avila/agents#147](https://github.com/danny-avila/agents/pull/147),
which makes `SubagentExecutor` inherit the parent invocation's
`configurable` (with `thread_id`/`run_id`/`parent_run_id` scrubbed)
into the child workflow. Subagent tool dispatches through the parent's
`ON_TOOL_EXECUTE` handler now arrive with parent's `requestBody`,
`user`, `userMCPAuthMap`, etc. — so `{{LIBRECHAT_BODY_*}}` placeholder
substitution and per-user MCP connection lookup work for subagent
tool calls the same way they do for the parent agent.
Note: `package-lock.json` will need an `npm install` refresh once
v3.1.78 lands on the registry. The user/user_id injection added in
PR #12950 stays as defense-in-depth.
* 🗑️ refactor: drop redundant user/user_id injection from `loadToolsForExecution`
`@librechat/agents@3.1.78` (via danny-avila/agents#147) makes
`SubagentExecutor` forward the parent's `configurable` verbatim into
the child workflow. Subagent `ON_TOOL_EXECUTE` dispatches now arrive
with parent's `user` / `user_id` already in `data.configurable` —
making the host-side injection added in #12950 a no-op.
Removes:
- The conditional `user: createSafeUser(req.user); user_id: req.user.id`
block in `loadToolsForExecution` (req.user.id-guarded so the
`'api-user'` fallback in Responses/OpenAI controllers is preserved).
- The unused `createSafeUser` import.
- The 4 unit tests covering the now-deleted behavior.
The merge in `handlers.ts` (`{ ...configurable, ...toolConfigurable }`)
still produces a `mergedConfigurable` with the right user identity for
both parent and subagent paths — the values just come from
`configurable` (forwarded by the SDK) rather than `toolConfigurable`.
Other fixes from #12950 stay (IUser.id narrowing, the env.ts /
google/initialize.ts / remoteAgentAuth.ts TS-warning fixes) — they
were independent of the subagent identity propagation issue.
* 📦 chore: update `@librechat/agents` to v3.1.78
This update reflects the transition from the development version `3.1.78-dev.0` to the stable release `3.1.78`. The package-lock.json has been refreshed to ensure consistency with the new version, including updated integrity checks and resolved URLs for the package. This change is part of ongoing improvements to enhance the functionality and stability of the agents module.
* 🔐 fix: Forward per-file `entity_id` through code-env priming
Skill files and persisted code-env files now carry their `entity_id` on
the in-memory file refs that seed `Graph.sessions`. Without this, an
execute call that mixes a skill file (uploaded with `entity_id=skillId`)
and a user attachment (uploaded with no `entity_id`) collapses onto a
single request-level entity at the codeapi authorization step and one
side 403s. With per-file `entity_id`, codeapi resolves sessionKey per
file and both authorize.
- `primeSkillFiles` / `primeInvokedSkills`: thread `entity_id` through
fresh-upload, cache-hit, and per-skill-batch paths in
`packages/api/src/agents/skillFiles.ts`.
- `primeFiles` (Code/process.js): parse `entity_id` from the persisted
`codeEnvIdentifier` query string once per iteration; forward through
`pushFile`, including the reupload path which re-parses the fresh
identifier returned by codeapi.
- Tests: extend `skillFiles.spec.ts` with two cases — fresh-upload
propagation and cached-hot-path parsing.
Companion PRs in flight on `@librechat/agents` (forward `entity_id`
through `_injected_files`) and codeapi (per-file authorization). All
three are wire-back-compat: an absent `entity_id` falls back to the
existing request-level resolution.
* 🔧 chore: Update dependencies in package-lock.json and package.json
- Bump `@librechat/agents` to version `3.1.78-dev.0` across multiple package files.
- Upgrade `@langchain/langgraph-checkpoint` to version `1.0.2` and update its peer dependency for `@langchain/core` to `^1.1.44`.
- Update `axios` to version `1.16.0` and `follow-redirects` to version `1.16.0`.
- Add `@types/diff` as a new dependency at version `7.0.2` and include `diff` at version `9.0.0` in the `@librechat/agents` module.
- Introduce optional peer dependency `@anthropic-ai/sandbox-runtime` for `@librechat/agents` with metadata indicating it is optional.
* 🐛 fix: Make skill code-env cache persistence observable
Two changes to surface the skill-bundle re-upload issue without
behavioral changes to tenant scoping (root cause to be confirmed via
the new warn log):
1. `primeSkillFiles` now awaits `updateSkillFileCodeEnvIds` instead of
firing-and-forgetting it. The prior shape could race with the next
prime (read-before-write) even when the bulkWrite itself succeeds,
producing a silent cache miss. Latency cost: ~10–50ms on first
prime; in exchange every subsequent prime can rely on the
identifier being persisted by the time it reads.
2. `updateSkillFileCodeEnvIds` now returns `{matchedCount, modifiedCount}`
from the underlying bulkWrite. `primeSkillFiles` warn-logs when
`modifiedCount < updates.length`, making any silent drop visible —
whether the cause is tenant filtering, a `relativePath` mismatch,
schema-plugin scoping, or something else. Prior shape returned
`Promise<void>` so any zero-modification result was invisible.
Tests:
- `skill.spec.ts`: real-MongoDB happy path (counts match), no-match
case (modifiedCount=0), and empty-input contract.
- `skillFiles.spec.ts`: deferred-promise harness proving the call
site awaits the persist (prime stays pending until the persist
resolves) and forwards partial-write counts.
Deliberately narrower than the original draft of this commit, which
also bypassed `tenantSafeBulkWrite` for the codeEnvIdentifier write
on the speculative diagnosis that tenant filtering was the cause.
That change was a behavior shift on tenant scoping without
confirmation; reverted pending real-world signal from the new warn
log.
* 🐛 fix: Justify await for skill code-env persistence under concurrency
The await on `updateSkillFileCodeEnvIds` isn't a defensive nicety —
it's load-bearing for cache effectiveness under concurrent priming.
Verified with an out-of-tree harness (`config/test-skill-cache.ts`,
not committed) that wires `primeSkillFiles` against a real codeapi
stack:
- With fire-and-forget (prior shape after this branch's revert):
back-to-back primes for the same skill miss the cache. Call N+1
reads SkillFile docs before Call N's write commits, sees no
`codeEnvIdentifier`, re-uploads, fires its own forget that Call N+2
also races. Steady-state stays in cache miss for the full burst.
- With await: the prime that does the upload commits its persist
before resolving, so the next concurrent prime observes the cache
pointer instead of racing the read. Latency cost ~10–50ms on the
upload prime; subsequent concurrent primes save an entire batch
upload.
In production with primes seconds apart this race is rare; at scale
with many users hitting the same skill in the same second it's the
difference between M and N×M uploads.
Updates the regression test to assert the await contract (deferred
persist promise → prime stays pending until persist resolves).
Comment in `skillFiles.ts` rewritten to document the concurrency
rationale rather than the weaker "race-with-next-prime" framing the
prior commit used.
* 🌩️ feat: CloudFront CDN File Strategy + signed cookies
Squashed from PR #12193:
- feat(storage): add CloudFront CDN file strategy
- feat(auth): add CloudFront signed cookie support
Note: package.json/package-lock.json dependency additions are intentionally
omitted from this commit and will be re-added via `npm install` after rebase
to avoid lock-file merge conflicts. The two new peer deps that need to be
re-installed are:
- @aws-sdk/client-cloudfront@^3.1032.0
- @aws-sdk/cloudfront-signer@^3.1012.0
Also fixes 4 missing destructured names in AuthService.spec.js
(getUserById, generateToken, generateRefreshToken, createSession) that
were referenced in tests but not imported from the mocked '~/models'.
* 📦 chore: install CloudFront SDK deps for PR #12193
Adds the two AWS CloudFront packages required by the rebased
CloudFront CDN strategy:
- @aws-sdk/client-cloudfront
- @aws-sdk/cloudfront-signer
Following the @aws-sdk/client-s3 pattern:
- api/package.json: regular dependency (runtime resolution)
- packages/api/package.json: peerDependency
Generated by `npm install` against the freshly rebased lock file
to avoid the merge conflicts that came from the original PR's
lock-file edits being made against an older base of dev.
* 🐛 fix: CI failures + review findings on CloudFront PR #12193
CI fixes
- Rename packages/data-provider/src/__tests__/cloudfront-config.test.ts
→ src/cloudfront-config.spec.ts. Jest's default testMatch picks up
__tests__/ directories even inside dist/, so the compiled .d.ts shell
was being executed as an empty test suite. Moving to .spec.ts (matching
the rest of the package) avoids the dist/ pickup.
- Add cookieExpiry: 1800 to CloudFront crud.test makeConfig: the schema
applies a default so CloudFrontFullConfig requires it.
Review findings addressed
- #1 (Codex + comprehensive): Normalize CloudFront domain with /\/+$/
regex (and key with /^\/+/ regex) in buildCloudFrontUrl, matching the
cookie code so resource policy and file URLs stay aligned even when
the configured domain has multiple trailing slashes. Added tests.
- #2: Move DEFAULT_BASE_PATH out of s3Config into shared
packages/api/src/storage/constants.ts. ImageService no longer imports
S3-specific config.
- #3: getCloudFrontConfig() returns Readonly<CloudFrontFullConfig> | null
to discourage mutation of the cached signing config.
- #4: Add cross-field refinement tests for cloudfrontConfigSchema
(invalidateOnDelete-without-distributionId,
imageSigning="cookies"-without-cookieDomain).
- #6: Revert unrelated MCP comment re-indentation in
librechat.example.yaml.
- #7: Add azure_blob to the strategy list comment.
Skipped
- #5 (extractKeyFromS3Url with CloudFront URLs): existing
deleteFileFromCloudFront tests already cover the path-equivalence
assumption; renaming the helper is real refactor work beyond this
PR's scope.
- #8, #9 (NIT, low confidence): leaving for author judgement.
* 🧹 chore: drop dead DEFAULT_BASE_PATH from s3Config test mock
After moving DEFAULT_BASE_PATH to ~/storage/constants, crud.ts no longer
reads it from s3Config — so the entry in the s3Config jest mock was
misleading dead config. The tests still pass because the unmocked real
constants module provides the value.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🧰 feat: add `createConcurrencyLimiter` promise utility
Lightweight, dependency-free FIFO concurrency limiter for bounding
parallelism of expensive async work that fans out from a single
producer. Tasks queue rather than reject when the cap is reached;
slots release on both fulfillment and rejection so a single failure
cannot stall the queue. Each task runs inside a thunk so timeouts
and other side effects do not start until a slot is acquired.
* ⚡ perf: bound concurrent office-HTML rendering for code artifacts
A tool result with N office files (DOCX/XLSX/XLS/ODS/CSV/PPTX)
previously fanned out into N parallel mammoth/SheetJS/PPTX renders,
all CPU-bound and synchronous. Under bursty agent output this
competes with the still-running inference loop for event-loop time
and inflates end-of-run "finalize" waits in non-streaming flows
(BaseClient chat-completions, non-streaming Responses, the tools.js
direct endpoint) which all `await Promise.all(artifactPromises)`
before responding.
Cap the parser layer at 2 concurrent office-HTML renders process-
wide via a shared `createConcurrencyLimiter`. Tasks queue in FIFO
and the per-render timeout starts only after a slot is acquired so
queue waits do not consume the timeout budget. The HTML-or-null
contract (no text fallback for office types) is preserved.
* 🐛 fix: Propagate User Identity to Subagent MCP Tool Calls
The `@librechat/agents` SDK's `SubagentExecutor` invokes the child
workflow with a fresh configurable of `{ thread_id }` only — the
parent's `user` / `user_id` are dropped on the way into the child
graph. The child's `ToolNode` then dispatches `ON_TOOL_EXECUTE` to the
parent's handler, which merges `{ ...configurable, ...toolConfigurable }`,
but neither side carries user identity for subagents.
Downstream MCP tools read `config.configurable.user?.id || user_id` and
got `undefined`, so `MCPManager.getConnection` fell through to the
"No connection found for server X" error path — it can't reach the
user-connection lookup without a userId.
Re-inject `user` (via `createSafeUser`) and `user_id` from `req.user`
into the configurable returned by `loadToolsForExecution`. This is the
single point all controllers (chat, Responses API, OpenAI-compat) flow
through. For the parent agent it's a no-op (outer config already
carries the same values); for subagents it fills the gap so MCP
connection lookup, user-placeholder substitution, and tools that read
configurable.user all work correctly.
* 🐛 fix: Preserve `api-user` Fallback When Injecting Subagent Identity
Codex review pointed out that the prior commit unconditionally wrote
`user_id: req.user?.id` (and `user`) into `toolConfigurable`. The handler
merges via `{ ...configurable, ...toolConfigurable }` — `toolConfigurable`
wins — so when `req.user` is absent, this overwrote the outer config's
`'api-user'` fallback (set by `responses.js` / `openai.js` for the
unauthenticated API-key path) with `undefined`, breaking MCP connection
lookup for that path.
Only inject the keys when `req.user.id` is truthy. Omitting them lets
the merge preserve whatever the outer configurable already had. Tests
updated to assert key omission for `req.user` undefined / null / present
without `id`.
* 🩹 fix: Narrow `IUser.id` to required string
`IUser` extends mongoose `Document`, which types `id?: any` (the optional
virtual). At runtime `id` is always `_id.toString()` for a hydrated doc,
so narrow the type to a required string.
Closes two `@rollup/plugin-typescript` TS2322 warnings introduced by
PR #12450 (OIDC Bearer Token Authentication for Remote Agent API)
where `req.user = userResolution.user` and the
`(req: Request, res: Response, next: NextFunction)` signature both
failed against the project's local `Express.User` augmentation
(`{ [key: string]: any; id: string; }`) because `IUser.id` was
`any`/optional. Narrowing here fixes both at the source rather than
casting at every assignment site.
* 🩹 fix: Resolve TS Build Warnings Surfaced by `IUser.id` Narrowing
Three rollup TS plugin warnings surfaced after narrowing
`IUser.id` from `any` to `string`:
- `utils/env.ts:95` — `safeUser[field] = user[field]` failed strict
checking because indexed write through a union-typed key collapses
the LHS to the intersection of all field write types (i.e.,
`undefined` when fields have mixed types). The previous `id?: any`
on IUser had been masking this. Switch to `Object.assign(safeUser,
{ [field]: user[field] })` which widens the assignment.
- `endpoints/google/initialize.ts:35` — `getUserKey({ userId:
req.user?.id, ... })` failed because `req.user?.id` is now
`string | undefined` (no longer `any`). Match the pattern already
used in `endpoints/openAI/initialize.ts:49`: `req.user?.id ?? ''`.
- `middleware/remoteAgentAuth.ts:465` — pre-existing, unrelated to
the IUser change. The local (gitignored) `express.d.ts` augments
`express.Request` but not `express-serve-static-core.Request`,
so the explicit `(req: Request, ...)` annotation imported from
`'express'` resolves to a Request whose `req.user` differs from
the one `RequestHandler` expects internally. Type the closure as
`RequestHandler` directly so TS infers params from the augmented
type.
* 🩹 fix: Cast `RemoteAgentAuth` Closure to `RequestHandler`
My previous attempt removed the explicit `req: Request` annotation on
the closure to side-step the outer `RequestHandler` mismatch. That
shifted the error to every helper call site inside the closure
(`getConfigOptions(req)`, `runApiKeyAuth(req, ...)` at 467/474/493/
512/531), because the helpers annotate their params with
`express.Request` (which has the local `Request.user` augmentation),
while the unannotated closure inferred `req` as
`express-serve-static-core.Request` (no augmentation). Reproduced
locally by stubbing the gitignored `src/types/express.d.ts`.
Right approach: keep the explicit `req: Request` annotation so the
closure body matches the helpers' types, then cast at the return —
`RequestHandler`'s internal `Request` resolves through
`express-serve-static-core` and lacks the augmentation, so the cast
is the boundary that bridges the two views of `req.user`.
Verified against a build with the local express.d.ts stub: zero
warnings on `remoteAgentAuth.ts`, `env.ts`, and `google/initialize.ts`.
* 📄 feat: Rich File Artifact Previews for DOCX, CSV, XLSX, PPTX
Render office files emitted by tools as interactive previews in the
artifact panel instead of raw extracted text. The backend produces a
sanitized HTML document via mammoth (DOCX), SheetJS (CSV/XLSX/XLS/ODS),
or yauzl-based slide extraction (PPTX) and ships it through the
existing SSE attachment payload; the client routes it through the
Sandpack `static` template's `index.html` slot — no new browser deps,
no client-side blob fetch, no React renderer components.
* 🔐 fix: Restrict data: URLs to <img> in office HTML sanitizer
Codex review on #12934 caught that `data:` lived in the global
`allowedSchemes`, which meant a smuggled `<a href="data:text/html,
<script>...</script>">` would survive sanitization. The Sandpack
iframe sandbox does not gate `target="_blank"` navigations, so a
click would open attacker-controlled HTML in a new tab.
Scope `data:` to `<img src>` only via `allowedSchemesByTag` (mammoth
inlines DOCX images as base64 `data:image/...` URIs — that path still
works). Add a regression suite (`sanitizeOfficeHtml security`) with
8 cases covering: <script> stripping, event-handler removal,
javascript:/data: rejection on anchors, data:image preservation in
<img>, http/https/mailto allowance, target=_blank rel=noopener
enforcement, and <iframe> stripping.
* 🔧 fix: Route extensionless office files by MIME alone
Codex review on #12934 caught that the office-render gate in
`extractCodeArtifactText` only fired when the extension was in
`OFFICE_HTML_EXTENSIONS` or the category was `document`/`pptx`. A tool
emitting `data` with `text/csv` (no extension) classifies as
`utf8-text`, so the gate was skipped and raw CSV text shipped to the
client — but the client routes by MIME to the SPREADSHEET bucket
expecting a full HTML document, so the panel rendered broken text.
Extract a shared `officeHtmlBucket(name, mime)` predicate from
`html.ts` (returns the bucket name or null). Both `bufferToOfficeHtml`
(the dispatcher) and the upstream gate in `extract.ts` now go through
this single source of truth, so they can never drift apart again. The
predicate already mirrors the dispatcher's extension/MIME logic
(extension wins; MIME is the fallback for extensionless inputs).
Adds:
- 14 cases for the new `officeHtmlBucket` predicate covering the
positive paths (each bucket via extension OR MIME) and the negative
paths (txt, py, json, jpg, pdf, zip, odt, plain noext).
- A direct regression test in `extract.spec.ts` for the Codex catch:
`data` with `text/csv` + utf8-text category routes through the
office HTML producer.
- Parameterized cases for extensionless DOCX/XLSX/XLS/ODS/PPTX files
identified by MIME alone.
* 🛡️ fix: Enforce extension-wins precedence in officeHtmlBucket
Codex review on #12934 caught that the predicate's if-chain interleaved
extension and MIME checks for each bucket — e.g. CSV's branch was
`ext === 'csv' || CSV_MIME_PATTERN.test(mimeType)`. A `deck.pptx`
shipped with `text/csv` (sandboxed tools sometimes ship generic MIMEs)
matched the CSV branch BEFORE the PPTX extension branch was reached,
so a binary PPTX would have been handed to `csvToHtml` to parse as
text — yielding garbage or a parse exception.
Restructure to a strict two-pass dispatch: an exhaustive extension
table first (one lookup, all known extensions), then MIME-only
fallback for extensionless / unknown-ext inputs. The doc comment's
"extension wins" claim is now actually enforced by the implementation.
Add 7 regression cases covering the conflicting-MIME footgun for each
bucket: deck.pptx + text/csv → pptx; workbook.xlsx + text/csv →
spreadsheet; legacy.xls + pptx-MIME → spreadsheet; report.docx +
text/csv → docx; data.csv + docx-MIME → csv; etc.
* 🛡️ fix: Reject zip-bomb office files before in-process parsing (SEC)
Addresses pre-existing availability vulnerability validated by
SEC review (Codex finding 275344c5...) and made worse by this PR's
HTML rendering path. A sub-1MiB compressed XLSX/DOCX/PPTX (highly
compressed run-of-zeros) inflates to 200+ MiB of XML when handed
to mammoth/xlsx — blocking the Node event loop for 10+ seconds and
spiking RSS to ~1 GiB. The existing 8s `withTimeout` wrapper uses
`Promise.race`, which can only return early; it cannot interrupt
synchronous parser CPU/RAM consumption. PoC ran an authenticated
execute_code call to OOM the API process.
Add `assertSafeZipSize(buffer)` — a yauzl-based pre-flight that
streams every entry with mid-inflate byte counting and bails on
either a per-entry or total decompressed-size cap. Mid-inflate
counting cannot be bypassed by falsifying the central directory's
`uncompressedSize` field (the technique the PoC used). Defaults:
25 MiB per entry, 100 MiB total — generous headroom for legitimate
image-heavy office files, well below the attack profile.
Hook the check into every path that hands a buffer to mammoth/xlsx
/yauzl:
- New HTML producers (`wordDocToHtml`, `excelSheetToHtml`,
`pptxToSlideListHtml`) — added by this PR
- Legacy RAG text extractors (`wordDocToText`, `excelSheetToText`
in `crud.ts`) — pre-existing path, also vulnerable
Errors propagate as a tag-distinct `ZipBombError` so callers can
distinguish a refused bomb from generic parse failures. The outer
`extractCodeArtifactText` swallows the error and returns null,
falling back to the regular download UI.
`.xls` (BIFF/CFB binary, not ZIP) is detected by magic bytes and
skipped — yauzl would reject it as malformed anyway.
Adds 15 tests:
- `zipSafety.spec.ts` (9): benign passes, per-entry cap, total cap,
ZipBombError type-tagging, malformed-zip distinction, directory-
entry handling, named-error surfacing, and the SEC-PoC pattern
(sub-1 MiB compressed → 50 MiB inflated rejected on default caps).
- `html.spec.ts` zip-bomb suite (5): each producer rejects a bomb;
dispatcher propagates correctly; legitimate fixtures still render.
- `extract.spec.ts` (1): outer extractor swallows ZipBombError and
returns null so the download UI fallback fires.
* 🧹 fix: Normalize MIME parameters; add legacy CSV MIME variant
Two related Codex catches on PR #12934 — both about MIME-routing
inconsistencies between backend and client that would cause
extensionless CSV files to render as broken (raw text under an HTML
slot) or skip the artifact panel entirely.
P2 — backend MIME normalization:
`officeHtmlBucket` matched MIME strings exactly, so a real-world
`text/csv; charset=utf-8` Content-Type slipped through and the
backend returned raw CSV text. The client's `baseMime` helper
strips parameters before its own MIME lookup, so it routed the
same file to the SPREADSHEET bucket expecting an HTML body that
never arrived. Mirror the client's normalization on the backend
(strip everything from `;` onward, lowercase) before bucket
matching.
P3 — client legacy CSV MIME:
Backend's `CSV_MIME_PATTERN` accepts three variants (`text/csv`,
`application/csv`, `text/comma-separated-values`); the client's
`MIME_TO_TOOL_ARTIFACT_TYPE` only had the first two. An
extensionless file with `text/comma-separated-values` would have
backend HTML produced but the client would skip the artifact
panel entirely. Add the missing variant.
Tests:
- 9 new parameterized-MIME cases on backend covering charset/
boundary/case variants for every bucket.
- 1 new client routing case for `text/comma-separated-values`.
* 🩹 fix: Try office HTML before short-circuiting on category=other
Codex review on #12934 caught that the early `category === 'other'`
return short-circuited before `hasOfficeHtmlPath` was checked. The
classifier returns 'other' for inputs the new dispatcher can still
route — extensionless `application/csv` (CSV MIMEs aren't in the
classifier's text-MIME set and don't start with `text/`), and
extensionless office MIMEs with parameters like `application/vnd...
spreadsheetml.sheet; charset=binary` (the classifier's `isDocumentMime`
exact-matches these MIMEs without parameter normalization). Both would
route correctly through `officeHtmlBucket` but never reached it.
Move the office-HTML attempt above the 'other' early return, and drop
the `|| category === 'document' || category === 'pptx'` shortcut now
that `hasOfficeHtmlPath` covers the same surface (with parameter
normalization) and a wider one. ODT still routes through `extractDocument`
unchanged — `hasOfficeHtmlPath` returns false for it and the
`category === 'document'` branch below handles it.
Adds 3 regression tests:
- extensionless `application/csv` + category='other' → office HTML
- extensionless parameterized office MIME + category='other' → office HTML
- defense check: actual binary 'other' (image/jpeg) still returns null
without invoking the office producer
* 🛡️ fix: Office types are HTML-or-null (no text fallback → XSS)
Codex P1 review on #12934 caught that when `renderOfficeHtml` failed
(timeout, malformed file, zip-bomb rejection) for an office type, the
extractor fell through to `extractDocument` and returned plain text.
The client routes by extension/MIME to the office preview buckets and
feeds `attachment.text` straight into the Sandpack iframe's
`index.html`. A spreadsheet cell or document body containing the
literal string `<script>alert(1)</script>` would have been injected
as executable markup — direct XSS.
The contract for office types is now HTML-or-null with no text
fallback. Failed render returns null, the client's empty-text gate
keeps the artifact off the panel, and the file falls back to the
regular download UI (matching what PPTX already did). PDF and ODT
still go through `extractDocument` because the client routes them to
PLAIN_TEXT (which the markdown viewer escapes) or no artifact at all,
so plain text is safe there.
Test reshuffle:
- `document` describe block now uses ODT/PDF for the legacy
parseDocument-path tests (DOCX/XLSX/XLS/ODS bypass that path).
- New "does NOT call parseDocument for office HTML types" test locks
in the SEC contract for all four office HTML buckets.
- "falls back to ..." tests rewritten as "returns null when ..." with
explicit `parseDocumentCalls.length === 0` assertions to prove no
text leaks back to the client.
- New XSS regression test for the XLSX failure path.
- Mock parseDocument failure-name match relaxed to `includes()` so
ODT-named tests can use the same trigger.
* 🧽 chore: Address follow-up review findings on PR #12934
Wraps up the 10-finding follow-up review. Two MAJOR + four MINOR + two
NIT addressed; one NIT skipped after verifying it was a misread of the
package.json structure.
MAJOR
- #1: Rewrite `renderOfficeHtml` JSDoc to document the HTML-or-null
contract explicitly. The pre-fix doc described a text-fallback path
that was the original XSS vector (commit b06f08a). A future
maintainer trusting the stale doc could reintroduce the fallback.
- #2: Replace byte-truncation of office HTML with a small "preview too
large" banner document. Cutting at a UTF-8 boundary lands mid-tag
(`<table><tr><td>con\n…[truncated]`) and ships malformed markup to
the iframe — unpredictable rendering, occasional broken layouts on
DOCX with embedded images / wide spreadsheets.
MINOR
- #4: Wrap `readSlidesFromZip`'s `zipfile.close()` in try/catch so a
close-time exception (mid-flight stream) doesn't replace the
original error. Mirrors the defensive pattern in zipSafety.ts.
- #5: Refactor PPTX extraction to use `yauzl.fromBuffer` directly,
eliminating the temp-file write/unlink the safety pre-flight already
proved unnecessary. Removes 4 unused imports (os, path, fs/promises,
randomUUID).
- #6: Extract `isPreviewOnlyArtifact(type)` to `client/src/utils/
artifacts.ts` so the membership check is unit-testable without
mounting the full Artifacts component (Recoil + Sandpack + media
query). 15 new test cases covering positive types, negative types,
null/undefined, and unknown strings.
NIT
- #3: Remove dead `stripColorStyles` / `COLOR_PROPERTY_PATTERN` —
unused (sanitizer's `allowedStyles` config handles color implicitly).
- #7: Remove dead `!_lc_csv_label` worksheet property write.
- #9: Remove no-op `exclusiveFilter: () => false` sanitize-html config.
- #10: Type-narrow `PREVIEW_ONLY_ARTIFACT_TYPES` to
`ReadonlySet<ToolArtifactType>` so the membership table is
compile-time checked against the enum.
SKIPPED
- #8: Reviewer flagged `sanitize-html` as duplicated in devDeps and
dependencies. The package has no `dependencies` section — only
`devDependencies` and `peerDependencies`. Existing convention
(mammoth, xlsx, yauzl, pdfjs-dist) is to appear in BOTH. Removing
the devDep entry would break local test runs.
Tests: packages/api 4406/4406, client artifacts 128/128.
* 🪞 chore: Fix isPreviewOnlyArtifact test description parameter order
Follow-up review nit on PR #12934. Jest's `it.each` substitutes `%s`
positionally, and the table rows were `[type, expected]` while the
description template read `'returns %s for type %s'` — outputting
"returns application/vnd.librechat.docx-preview for type true"
instead of the intended "type ... returns true".
Reorder the template to match the column order. Test runner output
now reads naturally: "type application/vnd.librechat.docx-preview
returns true". Pure cosmetic — runtime behavior unchanged.
* ✨ feat: Improve DOCX rendering and surface filename in panel header
Two UX improvements based on hands-on use of the office preview pipeline.
DOCX rendering — mammoth strips the navy banners, cell shading, and
column layouts that direct-formatted docs apply (python-docx-style
output is a common case). The flat `<p><strong>X</strong></p>` and
bare `<table><tr><td>` it emits looks washed out next to the source.
Three targeted compensations:
- Style map promotes `Title`, `Subtitle`, `Heading 1` thru `Heading 6`,
and `Quote` paragraphs to their semantic HTML equivalents (mammoth's
default only handles Heading 1-6, missing Title/Subtitle/Quote).
- Extra CSS scoped to `.lc-docx` gives the first table row sticky-
looking header styling regardless of `<thead>` (mammoth never emits
`<thead>`), adds zebra striping, and treats the python-docx
`<p><strong>X</strong></p>` section-heading idiom as a pseudo-h2 with
a thin accent left border so document structure survives the round
trip. Headings get a left accent or underline so they read as
headings instead of just bold paragraphs.
- Sanitizer's `allowedAttributes` opens `class` on the heading and
block tags the styleMap and CSS heuristics rely on. `<script>`,
event handlers, javascript: URLs, etc. are still stripped — the
existing security regression suite catches any drift.
Panel header — `Artifacts.tsx` showed a generic "Preview" pill for
preview-only artifacts. Single-tab Radio is a no-op; surfacing the
document filename there gives the user something useful in the chrome
without taking real estate. `displayFilename` handles the sandbox
dotfile suffix the upload pipeline applies.
Tests: html.spec.ts +1 (new CSS-emission lock), 71/71. Backend files
suite 428/428. Client 308/308.
* ✨ feat: High-fidelity DOCX preview via docx-preview in iframe
Switch the default DOCX render path from server-side mammoth → flat
HTML to client-side `docx-preview` loaded inside the Sandpack iframe.
Mammoth becomes the fallback for files above the cap.
Why
---
The Sandpack iframe is a real browser DOM. Server-side rendering
ceiling for DOCX→HTML is well below the source's visual fidelity —
mammoth strips cell shading, run colors, banners, and column layouts
because Word's layout model doesn't fit HTML's flow model. Pushing the
render into the iframe lifts that ceiling without paying the
server-side cost of jsdom or LibreOffice.
What
----
- New `wordDocToHtmlViaCdn(buffer)` builds a self-contained HTML doc
that embeds the binary as base64 and lets `docx-preview@0.3.7`
render it on load. CSS preserves dark/light mode handoff via
`prefers-color-scheme`. Bootstrap script falls back to a "preview
unavailable, please download" message if the CDN is unreachable or
the parse throws.
- `docx-preview` and its `jszip` peer dep are pinned to specific
versions on jsdelivr with SRI sha384 integrity hashes and
`crossorigin="anonymous"`. Refresh: re-fetch the file, run
`openssl dgst -sha384 -binary FILE | openssl base64 -A`.
- CSP locked down on the iframe: `default-src 'none'`, scripts only
from jsdelivr (no eval), `connect-src 'none'` so a parser bug in
docx-preview can't be turned into exfiltration of the embedded
document, `base-uri 'none'`, `form-action 'none'`. Defense in depth
on top of the Sandpack cross-origin sandbox.
- `wordDocToHtml` dispatches by size: ≤ 350 KB binary → CDN path
(high fidelity), larger → mammoth fallback (preserves the size cap
on `attachment.text`). 350 KB chosen so worst-case base64-inflated
output (~478 KB) plus wrapper overhead (~5 KB) fits under
MAX_TEXT_CACHE_BYTES (512 KB) with 40 KB headroom.
- Internal renderers exported as `_internal` for tests. Public API
unchanged — callers still go through `wordDocToHtml`.
PPTX intentionally NOT switched
-------------------------------
Surveyed the available client-side PPTX libraries:
- `pptx-preview@1.0.7` ships an ESM-only main entry plus a 1.36 MB
UMD that references `require("stream"/"events"/"buffer"/"util")` —
bundled for Node, not browser-clean. Could work but the runtime
references to undefined Node globals are a fragility risk worth
more validation than this PR can absorb.
- `pptxjs` is jQuery-era, requires four separate UMD scripts in a
specific order, less actively maintained.
- The honest answer for PPTX is the LibreOffice sidecar (DOCX/XLSX/
PPTX → PDF → PDF.js), which is the architecture every major
product (Google Drive, Claude.ai, ChatGPT) effectively uses and
the only path to ~5/5 fidelity for arbitrary user decks.
PPTX stays on the existing slide-list extraction for now. Open a
follow-up issue for the LibreOffice/Gotenberg sidecar.
Tests
-----
- 6 new in CDN-rendered describe block: wrapper structure, base64
round-trip, SRI integrity + crossorigin, CSP locks
(connect-src/eval/base-uri/form-action), fallback message wiring,
size-threshold lock.
- Adjusted 2 existing tests that asserted on mammoth-path artifacts
(literal document text in `<article class="lc-docx">`) — those
assertions move to the mammoth-fallback test that calls
`_internal.wordDocToHtmlViaMammoth` directly. Dispatcher tests now
assert CDN-path signatures instead.
packages/api files: 434/434 ✅, full unit suite 4473/4473 ✅.
* 🧷 fix: Address Codex P1 (MIME aliases) + P2 (CDN dependency)
Two follow-up review findings on PR #12934, both real.
P1 — Spreadsheet MIME aliases on client
----------------------------------------
Backend's `officeHtmlBucket` uses the broad `excelMimeTypes` regex from
`librechat-data-provider` (covers `application/x-ms-excel`,
`application/x-msexcel`, `application/msexcel`, `application/x-excel`,
`application/x-dos_ms_excel`, `application/xls`, `application/x-xls`,
plus the canonical sheet MIMEs). The client's exact-match
`MIME_TO_TOOL_ARTIFACT_TYPE` only had three of those, so an
extensionless XLS upload with a legacy MIME would have backend HTML
produced but the client would fail to route the artifact at all —
preview chip never registers.
Fix: import the same regex on the client and add it as a fallback in
`detectArtifactTypeFromFile` after the exact-match map miss. Stays in
lock-step with the backend automatically.
7 new test cases — one per legacy alias.
P2 — Hard CDN dependency on jsdelivr
-------------------------------------
Air-gapped / corporate-filtered networks where jsdelivr is unreachable
would see DOCX previews permanently degrade to "Preview unavailable"
because the iframe could never load the renderer scripts. Mammoth was
sitting right there on the server but the dispatcher always preferred
the CDN path for files under 350 KB.
Fix: `OFFICE_PREVIEW_DISABLE_CDN` env var. When truthy (`1`, `true`,
`yes`, case-insensitive, whitespace-trimmed), `wordDocToHtml`
short-circuits to the mammoth path regardless of file size. Operators
on filtered networks set the env var; default behavior is unchanged.
Read at function-call time (not module load) so jest can flip it in
`beforeEach` without `jest.resetModules()`. The cost is one property
access per render.
12 new test cases: env-unset uses CDN (default), all five truthy
forms force mammoth, five non-truthy forms (`false`/`0`/`no`/empty/
arbitrary string) leave CDN active.
Tests
-----
packages/api/src/files: 446/446 ✅ (was 434, +12 from env-var matrix).
client artifact suites: 235/235 ✅ (was 228, +7 from MIME aliases).
* ✨ feat: High-fidelity PPTX preview via pptx-preview in iframe
Mirrors the DOCX CDN architecture for PPTX: small files (≤350 KB
binary) embed as base64 and render via `pptx-preview` loaded from
jsdelivr inside the Sandpack iframe. Larger files and air-gapped
deployments fall back to the existing slide-list extraction.
Why
---
PPTX is the format where the gap between LibreChat's preview and
Claude.ai-style previews was most visible (slide-list of bullet
points vs. rendered slide layouts). LibreOffice → PDF → PDF.js is
still the eventual gold-standard answer for PPTX fidelity, but
client-side rendering inside the Sandpack iframe gets us a
meaningful intermediate step (~1.5/5 → ~3.5/5) without a sidecar.
What
----
- `pptx-preview@1.0.7` (ISC license, ~1.36 MB UMD bundle that
includes its echarts/lodash/uuid/jszip/tslib deps inline). Pinned
to a specific version on jsdelivr with SHA-384 SRI and
`crossorigin="anonymous"`.
- `buildPptxCdnDocument` mirrors the DOCX wrapper: same CSP locks
(`default-src 'none'`, `connect-src 'none'`, no eval, no base/form
tampering), same `id="lc-doc-data"` base64 slot, same fallback
message wiring (`typeof pptxPreview === 'undefined'` →
"Preview unavailable").
- New public `pptxToHtml(buffer)` dispatcher; `bufferToOfficeHtml`
switches its `'pptx'` case to call it. `pptxToSlideListHtml` stays
exported as the slide-list-only path (still hit by tests directly
and by the dispatcher fallback).
- `OFFICE_PREVIEW_DISABLE_CDN=true` env-var hatch applies to PPTX
too — air-gapped operators get the slide-list path. Same env-var
read at call time, same matrix of truthy values (`1` / `true` /
`yes` / case-insensitive / whitespace-trimmed).
- `_internal` re-exports moved to after the PPTX section since the
PPTX internals live further down in the file. Adds
`pptxToHtmlViaCdn`, `MAX_PPTX_CDN_BINARY_BYTES`,
`PPTX_PREVIEW_CDN`.
Honest caveats
--------------
- The 1.36 MB UMD bundle has `require("stream"/"events"/"buffer"/
"util")` references in its outer wrapper. Those are bundled-dep
artifacts (likely from `tslib` / Node-shim transforms) and don't
appear to execute on the browser code paths, but I haven't done
manual e2e on a wide range of decks. If a class of files turns up
that breaks rendering, the iframe-side fallback message catches it
and operators have `OFFICE_PREVIEW_DISABLE_CDN=true` as the bail.
- First-render CDN fetch is ~1.36 MB (browser-cached after).
- PPTX with embedded media easily exceeds the 350 KB binary cap;
those files take the slide-list path. Lifting the cap is a
follow-up (tied to the broader self-hosting work).
Tests
-----
11 new in two new describe blocks:
- `pptxToHtml dispatcher`: routing predicate (small → CDN, env-set
→ slide-list).
- `CDN-rendered path`: base64 round-trip, SRI integrity +
crossorigin, CSP locks (connect/eval/base/form), fallback message,
size-threshold lock at 350 KB.
- `OFFICE_PREVIEW_DISABLE_CDN escape hatch`: env-var matrix for
truthy values.
packages/api/src/files: 457/457 ✅ (was 446, +11).
* 🪟 fix: DOCX preview fills the artifact panel width
docx-preview defaults to rendering at the document's native page
width (8.5in for letter, 21cm for A4). In a wide artifact panel
that left whitespace on either side; in a narrow one it forced
horizontal scroll.
Two changes:
- Pass `ignoreWidth: true` to `docx.renderAsync` so the library skips
the document's pageSize width and uses its container's width.
- Defensive CSS overrides on `.docx-wrapper` and `.docx-wrapper > section.docx`
in case a future library version regresses on the option, plus
`padding: 0` on the wrapper to drop the page-edge whitespace
docx-preview otherwise reserves.
`renderHeaders`/`renderFooters`/etc. stay enabled — those still
appear in the rendered output, just inside a container that fills
the panel instead of a fixed-width "page."
Tests unchanged (100/100); manual e2e ahead of merge.
* 🩹 fix: PPTX black screen — allow blob: workers + harden bootstrap
Manual e2e of the PPTX CDN renderer surfaced a black screen with
"Could not establish connection. Receiving end does not exist."
unhandled-rejection — characteristic of a Web Worker that couldn't
start.
Root cause: pptx-preview's bundled echarts dep spins up Web Workers
via blob: URLs for chart rendering. Our CSP had `default-src 'none'`
and no `worker-src`, so workers fell back to default → blocked. The
async failure deep inside echarts didn't surface through the outer
`previewer.preview()` promise, so my bootstrap's `.catch` never fired,
the loading state was removed, and the iframe sat with the body
background showing through (dark navy in dark mode = "black screen").
Three changes:
- Add `worker-src blob:` to the PPTX CSP. Allows blob:-only worker
creation without permitting arbitrary worker URLs.
- Bootstrap: window-level `unhandledrejection` and `error` listeners
so rejections from inside bundled-dep async pipelines surface as
the user-facing "Preview unavailable" fallback instead of going
silent.
- Bootstrap: 8-second timeout that checks `container.children.length`
— if the renderer hasn't appended anything visible by then, assume
silent failure and show the fallback.
Also wipe `container.innerHTML` when showing the fallback so a partial
render doesn't compete with the message.
DOCX wrapper unchanged: docx-preview doesn't use workers, so the
worker-src directive doesn't apply, and the existing fallback path
already covers its failure modes.
Tests
-----
- Existing PPTX CSP test now also asserts `worker-src blob:` is present.
- Existing fallback-message test extended to cover the new
unhandledrejection/error/timeout listeners.
packages/api/src/files: 467/467 ✅.
* 🔒 fix: gate office HTML routing on backend trust flag (textFormat)
Codex P1 review on PR #12934: routing .docx/.csv/.xlsx/.xls/.ods/.pptx
into the office preview buckets assumed `attachment.text` was already
sanitized full-document HTML, but that guarantee only existed for the
new code-output extractor path. Existing stored attachments and other
non-code paths can still carry plain extracted text — `useArtifactProps`
would then inject that as `index.html` inside the Sandpack iframe.
Adds a `textFormat: 'html' | 'text' | null` trust flag persisted on
the file record by the code-output extractor, surfaced over the SSE
attachment payload and the TFile API type. The client's routing in
`detectArtifactTypeFromFile` requires `textFormat === 'html'` before
landing on an office HTML bucket; everything else (legacy attachments,
RAG-extracted plain text from `parseDocument`, explicitly-marked
'text' entries) falls back to the PLAIN_TEXT bucket where the
markdown viewer escapes content rather than executing it.
Tests: new `getExtractedTextFormat` helper has 14 cases covering all
office paths, legacy XLS MIME aliases, parseDocument fallthroughs,
and null-input. Client `artifacts.test.ts` adds three security-gate
tests proving downgrade behavior for missing/null/'text' textFormat,
plus a `fileToArtifact` test that legacy office attachments without
the flag end up in PLAIN_TEXT with their content escaped.
* 🌐 fix: air-gapped DOCX preview — embed mammoth fallback in CDN doc
Codex P2 review on PR #12934: the CDN-rendered DOCX path always pulled
docx-preview + jszip from cdn.jsdelivr.net. Air-gapped or corporate-
filtered networks where jsdelivr is blocked would degrade to a static
"Preview unavailable" message even though the server already had a
local mammoth renderer that could produce readable output.
Now the dispatcher renders mammoth first and embeds the sanitized
output inside the CDN document as a hidden `#lc-fallback` block. The
iframe's existing `typeof docx === 'undefined'` check (which fires
when the CDN scripts can't load) un-hides the fallback so the user
sees a real preview. CDN-success path is unchanged: high-fidelity
docx-preview output owns the viewport, mammoth fallback stays hidden.
Two new safeguards in the dispatcher:
- Size budget: if base64(binary) + mammoth body + wrapper > 512 KB
(the `attachment.text` cache cap), drop to mammoth-only so a giant
document still renders. The `OFFICE_HTML_OUTPUT_CAP` constant
mirrors `MAX_TEXT_CACHE_BYTES` from extract.ts (separate constant
to avoid a circular import; pinned by a unit test).
- `lc-render` is hidden when fallback shows so the empty padded slot
doesn't sit above the mammoth content.
Tests: existing CDN-path tests updated for the new
`wordDocToHtmlViaCdn(buffer, mammothBody)` signature; new test for
the embedded fallback structure (`#lc-fallback`, mammoth body
content, "High-fidelity renderer unavailable" notice, render-slot
hide); new constant pin and per-fixture cap-respect assertion.
* 🧪 feat: LibreOffice → PDF preview path (POC, opt-in via env)
Per the plan-mode discussion: prove out a LibreOffice subprocess
pipeline as an alternative to the docx-preview / pptx-preview CDN
renderers. LibreOffice handles every office format Microsoft and
LibreOffice itself can open (DOCX, PPTX, XLSX, ODT, ODP, ODS, RTF,
many more), produces a PDF, and the host browser's built-in PDF
viewer renders it inside the Sandpack iframe via a `data:` URI.
No client-side JS dependency, no CDN dependency, true high
fidelity for any feature LibreOffice supports.
Off by default. Operators opt in by setting both:
- `OFFICE_PREVIEW_LIBREOFFICE=true`
- LibreOffice (`soffice` or `libreoffice`) on the server's `$PATH`
When either is missing, the dispatcher falls through to the
existing CDN/mammoth/slide-list pipeline so a misconfiguration
doesn't break previews.
Hardening (`packages/api/src/files/documents/libreoffice.ts`):
- Fresh subprocess per call with isolated temp dir, stripped env
(PATH/HOME/TMPDIR only), and `-env:UserInstallation` so concurrent
conversions can't collide on shared `~/.config/libreoffice` locks
- 30-second wall-time cap; SIGKILL on timeout
- 50 MB PDF output cap to bound disk pressure
- 512 KB output cap on the wrapped HTML so the SSE/cache contract
stays intact (base64 inflates ~33%, effective PDF cap ~380 KB)
- Macros disabled by default flags (`--norestore --invisible
--nodefault --nofirststartwizard --nolockcheck`)
- Tag-distinct `LibreOfficeUnavailableError` /
`LibreOfficeConversionError` so callers can swallow appropriately
Iframe wrapper (`buildPdfEmbedDocument`):
- Native browser PDF viewer via `<iframe src="data:application/pdf;
base64,...">` — works in Chrome, Edge, Safari, Firefox
- CSP locks the iframe to `default-src 'none'; frame-src data:;
connect-src 'none'; script-src 'unsafe-inline'` — no outbound
network, no eval, no external scripts
- `#view=FitH` for first-paint sizing
- 4-second heuristic timer that swaps to a "Preview unavailable"
fallback when the browser's PDF viewer is disabled (kiosk mode,
Brave Shields, etc.)
Wired into `wordDocToHtml` and `pptxToHtml` as the first branch —
returns null when disabled / unavailable / oversized so the existing
pipeline takes over. XLSX intentionally NOT routed through this
path: SheetJS's HTML output is already excellent for spreadsheets
(sortable, sticky headers) and PDF rendering of sheets is awkward.
Tests (`libreoffice.spec.ts`, 30 cases — 25 always run, 5 conditional
on the binary): env-gating parser semantics matching
`OFFICE_PREVIEW_DISABLE_CDN`, fallthrough contract (never throws,
returns null on any failure), CSP lock-down, fallback structure,
binary probe caching + missing-binary path, error tagging, and
integration tests that engage when `soffice`/`libreoffice` is on
PATH (DOCX→PDF, PPTX→PDF, output-cap fallthrough). Integration
tests skip cleanly on bare CI.
* 🩹 fix: CI — preserve legacy download path for empty-text office attachments
Two regressions surfaced after the textFormat security gate landed.
1. **Client** (`LogContent.test.tsx` "falls back to the legacy download
branch for an office file with no extracted text"):
When the security gate downgraded an office type without
`textFormat: 'html'` to PLAIN_TEXT, the lenient empty-text gate on
PLAIN_TEXT then accepted a missing `text` field and rendered a
half-empty panel card. The historical contract is "office type +
no text → legacy download UI"; the downgrade should only fire when
there's actual plain text that needs safe-escaping.
Fix in `detectArtifactTypeFromFile`: short-circuit to null when the
office type lands in the security-gate branch with no text. The
PLAIN_TEXT downgrade still fires for legacy attachments that DO
carry plain text.
2. **API** (`process.spec.js` + `process-traversal.spec.js`): the
`@librechat/api` mocks didn't expose `getExtractedTextFormat`, so
`processCodeOutput` called `undefined(...)` → TypeError → tests got
undefined results. Added the helper to both mocks with a faithful
default (returns 'text' for non-null extractor output, null
otherwise).
Tests: new regression in `artifacts.test.ts` pinning the empty-text
+ no-textFormat → null contract for all four office types
(.docx/.csv/.xlsx/.pptx), so a future refactor can't silently
re-introduce the half-empty card.
* 🩹 fix: PPTX slides scale to fit panel width (no horizontal scroll)
Manual e2e on PR #12934: pptx-preview rendered slides at their native
init dimensions (960×540 default). The artifact panel is much narrower
than that, so the iframe got a horizontal scrollbar and only a corner
of each slide showed at any time — the user had to drag-scroll across
each slide to read it.
Fix: keep pptx-preview's init at 960×540 so its internal layout math
stays correct, then post-process each rendered slide:
- Cache the slide's native width/height on its dataset BEFORE
applying any transform (so subsequent re-fits don't measure the
already-transformed box).
- Wrap the slide in `.lc-slide-wrap` with explicit width/height set
inline to the scaled dimensions; the wrap shrinks the layout space
the slide occupies.
- Apply `transform: scale(panel_width / 960)` to the slide itself
with `transform-origin: top left` so the rendered output shrinks
from the top-left corner into the wrap.
- Cap the scale at 1.0 so small slides don't upscale and get blurry.
Streaming + resize:
- `MutationObserver` watches the container for slide insertions so
streaming renders get scaled on arrival rather than waiting for
the entire `previewer.preview` promise to settle.
- `ResizeObserver` re-fits all wrapped slides when the iframe
resizes (panel drag, window resize).
Tests: new "bootstrap wraps + scales each slide" lock in the wrap
class, scale computation, observer setup, and native-size caching
so a future refactor can't silently re-introduce the overflow.
* 🩹 fix: PPTX wrap+scale runs after preview, not during streaming
Manual e2e on PR #12934: regenerated PPTX showed "Preview unavailable"
in the iframe. Root cause: the MutationObserver I added in the
previous commit fired during pptx-preview's render and moved slides
out from under the library's references. pptx-preview's async
pipeline raised an unhandled rejection, the iframe's window-level
listener caught it, and the fallback message replaced the partial
render.
Fix: drop the MutationObserver. Apply the wrap+scale ONCE in a
`finalize` step that runs:
- On `previewer.preview().then` (the happy path)
- On the 8-second timeout safety net IF the container has children
(silent-failure path — pptx-preview emitted slides but never
resolved its outer promise)
To prevent the user from seeing an unscaled flash while pptx-preview
renders into the 960px-wide canvas, the container is set to
`visibility: hidden` at init and only revealed inside `finalize`
after wrap+scale completes.
Resize handling stays via `ResizeObserver` on `document.body`,
installed AFTER the wrap pass so it doesn't fire during the wrap
itself.
Tests: regression assertion now also locks in:
- `container.style.visibility = 'hidden' / 'visible'` (the flash-
prevention contract)
- Absence of MutationObserver (the bug we just removed — must NOT
creep back in via a future "let's scale during streaming" idea)
* 🩹 fix: PPTX slides fill panel width (drop upscale cap, per-slide scale)
Manual e2e on PR #12934: slides rendered correctly but didn't fill the
artifact panel — whitespace on either side. Two issues:
1. The scale was capped at `Math.min(1, available / SLIDE_W)`. On
panels wider than 960px, the cap clamped the scale to 1.0 and
slides rendered at native size with whitespace on the sides
instead of stretching.
2. The scale was computed against the constant `SLIDE_W = 960`, but
pptx-preview can emit slides whose `offsetWidth` differs from the
init param if the source PPTX has a non-16:9 layout. Per-slide
division of `available / nativeW` handles that case.
Fix: replace `computeScale()` with two helpers — `availableWidth()`
returns the panel content-box width and `scaleFor(nativeW)` returns
the per-slide scale. No upscale cap. The slide content is rendered
by pptx-preview against its 960×540 canvas using vector text /
canvas — scaling up to e.g. 1500px doesn't visibly degrade quality.
Tests: regression now also asserts:
- `availableWidth()` and `scaleFor()` exist by name
- The exact scale formula `availableWidth() / (nativeW || SLIDE_W)`
- Negative assertion that `Math.min(1, ...)` is NOT present, so a
future "let's add an upscale cap" rewrite can't silently
re-introduce the whitespace.
* 🩹 fix: PPTX preview fills panel height (no white gap below slides)
Manual e2e on PR #12934: PPTX preview filled the panel width but left
empty space below the last slide. DOCX didn't have this issue because
its content (mammoth-rendered HTML) flows naturally and either fits
exactly or overflows; PPTX slides are fixed-aspect 16:9 and don't
grow with the panel.
Two changes:
1. **Body fills the iframe viewport** — `html, body { min-height:
100vh }` plus `body { display: flex; flex-direction: column }`
and `#lc-render { flex: 1 0 auto }`. The dark theme bg now fills
the iframe even when total slide content is shorter than the
panel, so a single-slide deck never reveals a "white below" gap.
2. **Per-slide scale honors viewport height** — `scaleFor(nativeW,
nativeH)` now returns `min(width-fit, height-fit)` (largest
factor that fits without overflowing either dimension). On a
tall artifact panel with a short deck, slides grow up to the
full panel height instead of staying at the width-bound size.
Existing height-fit was always considered correct conceptually
but the previous implementation only used width-fit, leaving
half the viewport unused per slide.
Tests: regression now also asserts `availableHeight()`, the
`Math.min(sw, sh)` formula, and `min-height: 100vh` are in the
bootstrap. Negative assertion for the old `Math.min(1, ...)` upscale
cap remains.
* 🩹 fix: revert body flex on PPTX bootstrap (caused black-screen render)
Manual e2e regression on PR #12934: the previous commit added
`body { display: flex; flex-direction: column }` plus
`#lc-render { flex: 1 0 auto }` to fill the panel height. Side effect:
pptx-preview's internal layout assumes block flow on its ancestor
elements; making body a flex container caused slides to render as
solid-black rectangles (sized correctly, but with no visible content
inside).
Fix: keep just `html, body { min-height: 100vh }` for the bg-fill
effect — that alone gives empty space below short decks the dark
theme bg without changing flow. Drop the body-flex and the
`#lc-render { flex: 1 0 auto }` directives.
The height-aware `scaleFor(nativeW, nativeH)` from the same commit
stays — it doesn't interact with pptx-preview's layout, just chooses
a per-slide scale. Each slide still grows to fit the viewport
contain-style.
Negative-assertion added to the regression test: `body { display:
flex }` must NOT appear in the bootstrap, so a future "let's flex
the body to make height work" rewrite can't silently re-introduce
this.
(Note: the user also flagged DOCX theming as faint body text; I'm
leaving that for now per their note that it may be pre-existing.
Not addressed in this commit.)
* 🩹 fix: revert PPTX height-fill changes; lock DOCX CDN to light scheme
Two fixes for separate manual e2e regressions on PR #12934.
**1. PPTX black screen (single slide rendering as solid black).**
The previous fix removed `body { display: flex }` thinking that was
the sole cause, but the regression persisted. Bisecting against the
last known-good commit (4e2d538b0, width-fit only), the actual culprit
is the COMBINATION of:
- `min-height: 100vh` on html/body
- `availableHeight()` reading viewport-derived dimensions
- `Math.min(sw, sh)` height-aware scale
pptx-preview's CSS injection step interacts unpredictably with
these. Reverting to width-only `scaleFor(nativeW)` and dropping the
viewport min-height restores reliable rendering. Vertical empty
space below short decks now shows the body's bg color (`var(--bg)`)
which still matches the panel theme — that's an acceptable trade-off
vs. the black-screen regression.
Negative assertions added: `Math.min(sw, sh)`, `availableHeight`,
`min-height: 100vh`, `body { display: flex }` must NOT appear in
the bootstrap. So a future "let's fill height" rewrite has to
demonstrate it doesn't break pptx-preview before it can land.
**2. DOCX body text rendering as faint / translucent grey.**
docx-preview emits page-style rendering with white pages and the
docs native text colors. The CDN doc declared
`color-scheme: light dark`, so on OS dark mode the iframes
inheritable `--fg` resolved to `#e5e7eb` (light grey). docx-preview
body text (no explicit color in the source DOCX) inherited that
light-grey on the white page bg → barely-visible "translucent"
rendering.
Fix: declare `color-scheme: light` only in `buildDocxCdnDocument`,
drop the dark-mode `@media` override. docx-preview is a light-mode-
only renderer; matching that produces correct contrast regardless
of OS theme. The mammoth-only `wrapAsDocument` path is unaffected
— it owns its own bg + text colors and continues to respect the
users OS scheme.
New regression test pins the lock: CDN doc must contain
`color-scheme: light`, must NOT contain `color-scheme: light dark`,
must NOT contain `prefers-color-scheme: dark`.
* 🩹 fix: relax connect-src to allow sourcemap fetches (silence CSP noise)
Manual e2e on PR #12934: every time DevTools is open while viewing a
DOCX or PPTX preview, the console fills with CSP violations like:
Connecting to 'https://cdn.jsdelivr.net/npm/docx-preview@0.3.7/
dist/docx-preview.min.js.map' violates the following Content
Security Policy directive: "connect-src 'none'". The request has
been blocked.
The actual rendering isn't affected (sourcemap fetches happen AFTER
the script has already loaded and executed via `script-src`), but
the noise is enough to make people suspect a real problem and
distracts from useful console output.
Fix: relax `connect-src` from `'none'` to `'self' https://cdn.
jsdelivr.net` in both DOCX and PPTX CDN docs. This allows:
- Same-origin fetches (sandpack-static-server) — covers any
bundler-embedded sourcemaps + same-origin runtime fetches the
renderer might make
- jsdelivr fetches — covers sourcemaps from the CDN where we
loaded the script
Exfiltration risk stays minimal: the iframe is cross-origin to
LibreChat so an attacker can't read application data anyway, and
neither 'self' (sandpack-static-server) nor jsdelivr is a useful
target for exfiltrating slide content to a host the attacker
controls.
Tests updated: assertions for `connect-src 'none'` swapped to
`connect-src 'self' https://cdn.jsdelivr.net` for both DOCX + PPTX
CDN docs. Added negative assertion for wildcard `*` in connect-src
so a future "let's allow everything" rewrite can't widen the
exfiltration surface.
* 🩹 fix: surface PPTX/DOCX fallback reason (inline + console)
Manual e2e on PR #12934: "Preview unavailable" appears in the iframe
with no way to know what actually failed. The reason was tucked into
the fallback element's `title` attribute (hover-only tooltip) — easy
to miss and impossible to copy/paste.
Now surfaces three ways:
1. Visible inline via a `<details>` element with the reason in
monospace, folded so the friendly message stays primary but the
diagnostic is one click away in the iframe itself.
2. `title` attribute (preserved) for hover tooltip.
3. `console.error('[pptx-preview] fallback fired:', reason)` so
DevTools shows it in red — also the only reliable way to see
the reason if the iframe is detached / re-mounted.
DOCX gets the same console mirror (as `console.warn` since the
fallback there is "high-fidelity unavailable, showing simplified
preview" — informational, not error). The DOCX fallback already
displays the mammoth-rendered content visibly, so no `<details>`
needed there.
Tests: regression assertions pin the diagnostic surfacing — the
`<details>` element, the `title` write, and the `console.error`
call must all be present in the bootstrap.
* 🩹 fix: PPTX CDN embeds slide-list fallback + detects empty renders
Manual e2e + DOM inspection on PR #12934: pptx-preview silently
produces empty `.pptx-preview-wrapper` placeholders for pptxgenjs-
generated decks. The library parses the file enough to create the
960×540 host element with a black bg, then fails to populate it.
The outer Promise resolves "successfully" — no throw, no rejection,
the bootstrap thinks rendering succeeded — and the user sees a black
rectangle with no content and no fallback message.
Fix mirrors the DOCX mammoth-fallback pattern from commit 0c0b0ce88:
1. **Server side**: `pptxToHtml` now renders the slide-list body
(`<ol class="lc-pptx-list">...`) via the new `renderPptxSlidesBody`
helper, then embeds it inside the CDN doc via the new
`buildPptxCdnDocument(base64, slideListFallbackBody)` signature.
Combined-doc size budget mirrors the DOCX pattern: if the CDN doc
would exceed `OFFICE_HTML_OUTPUT_CAP` (512 KB), drop to slide-list
only.
2. **Iframe bootstrap**: new `hasRenderedContent()` check after
`wrapSlides()` walks each `.lc-slide-wrap` looking for actual
child content inside pptx-preview's emitted slide nodes. If every
wrap is empty, fires `showFallback('renderer-produced-empty-
wrappers ...')` which reveals the embedded slide-list view
instead of the previous static "Preview unavailable" message.
3. **CSS**: slide-list rules extracted to `PPTX_SLIDE_LIST_CSS`
constant so they can be inlined into both the standalone slide-
list document AND the CDN doc's `<style>` block (CSP `style-src`
is `'unsafe-inline'` only — no external sheets).
`renderPptxSlidesHtml` now delegates to `renderPptxSlidesBody`
wrapped in `wrapAsDocument` — single source of truth for the slide
markup.
Tests (506 passing, +1 vs before): existing `pptxToHtmlViaCdn`
call sites updated for the new fallback-body argument; new
regression test pins `hasRenderedContent`, the
`renderer-produced-empty-wrappers` reason string, the embedded
fallback structure, and the inlined slide-list CSS.
* fix: Detect Empty PPTX Preview Slides
* 🩹 fix: LibreOffice PDF embed uses blob: URL (Chrome blocks data: PDFs)
Manual e2e on PR #12934: enabling `OFFICE_PREVIEW_LIBREOFFICE=true`
on a host with `soffice` installed surfaced "This page has been
blocked by Chrome" inside the PDF preview iframe.
Root cause: Chrome blocks `data:application/pdf;base64,...`
navigations inside sandboxed iframes (anti-phishing measure since
Chrome 76, see crbug.com/863001). The Sandpack iframe IS sandboxed
(its `sandbox="..."` attribute lacks `allow-top-navigation` for
data: URLs specifically), so when our inner `<iframe src="data:
application/pdf;...">` tries to navigate, Chrome's interstitial
fires and renders the "blocked" message.
Fix: switch from `data:` URL to `blob:` URL. The bootstrap now:
1. Reads the base64 payload from a `<script type="application/
octet-stream;base64">` data block (same pattern as the DOCX
and PPTX wrappers).
2. Decodes via `atob` + `Uint8Array.from`.
3. Creates a `Blob` with `type: 'application/pdf'`.
4. `URL.createObjectURL(blob)` produces a same-origin blob: URL.
5. Sets `pdfFrame.src = url + '#view=FitH'` — Chrome treats blob:
URLs as legitimate navigation and serves the built-in PDF
viewer.
CSP updated: `frame-src blob:` (was `frame-src data:`). `data:` is
now explicitly NOT allowed in `frame-src` since Chrome would block
it anyway in our context — keeping it would be misleading
documentation.
Bonus: failure paths now log to `console.error` with a
`[libreoffice-pdf]` prefix so DevTools surfaces blob-creation
failures and PDF-viewer load timeouts in red.
Tests updated:
- "emits a complete sandboxed HTML document" now asserts the
data-block + blob URL construction (not the old data: URL).
- New CSP test "allows blob: in frame-src (NOT data:)" with both
positive and negative assertions to lock in the change.
- Integration test for `tryLibreOfficePreview` updated to look for
the data block + `URL.createObjectURL` instead of the data: URL.
- Large-payload test now verifies the data block round-trip rather
than data: URL escaping (base64 alphabet has no characters that
break out of `<script>` anyway).
* 🩹 fix: LibreOffice PDF embed renders via pdf.js (Chrome blocks blob: PDFs too)
Manual e2e on PR #12934 round 2: switching from `data:` to `blob:`
URLs (commit d90f26c11) didn't fix the "This page has been blocked
by Chrome" interstitial. Chrome blocks BOTH data: AND blob: PDF
navigations inside sandboxed iframes — the built-in PDF viewer
requires a top-level browsing context. The Sandpack host iframe is
sandboxed, so neither approach works.
Fix: switch from native browser PDF viewer to pdf.js (Mozilla's
pdfjs-dist) loaded from CDN. pdf.js renders to `<canvas>` which
works in any context — no plugin, no privileged viewer, no
top-level requirement. ~1 MB CDN load is acceptable for a path
that's already opt-in via `OFFICE_PREVIEW_LIBREOFFICE=true`.
Implementation:
- Pin pdf.js v3.11.174 (single-file UMD; v4+ uses ES modules which
complicate the load + SRI flow)
- Worker URL pointed at the same jsdelivr origin; CSP `worker-src
https://cdn.jsdelivr.net blob:` allows it
- DPR-aware canvas rendering: scale based on `panelWidth /
page.viewport.width * devicePixelRatio` so retina displays get
crisp pixels
- Sequential page rendering (Promise chain) so a many-slide PDF
doesn't spawn N parallel render jobs
- 15 s timeout safety net (was 4 s for the native viewer; pdf.js
with DPR=2 on a many-page PDF can take longer)
CSP changes:
- Added `script-src https://cdn.jsdelivr.net 'unsafe-inline'` (was
inline-only)
- Added `worker-src https://cdn.jsdelivr.net blob:`
- Removed `frame-src` entirely (no nested iframes)
- Removed `object-src` (no `<object>`/`<embed>` either)
Same diagnostic surfacing as the other CDN paths: failure reasons
shown via `<details>` disclosure inline + `console.error` to
DevTools.
Tests updated: PDF.js script presence, GlobalWorkerOptions setup,
canvas render path, all the new failure detection paths. Negative
assertions for both `data:application/pdf` and `blob:...application
/pdf` so a future "let's just try the native viewer again" rewrite
can't silently re-introduce the Chrome block.
SRI hashes intentionally omitted (unlike docx-preview / pptx-
preview) — operator opted in by setting the env flag and trusts
the LibreOffice render pipeline. Worth adding once the path is
proven in production.
* 🧹 cleanup: trim unused _internal exports + stale JSDoc references
After the LibreOffice + pdf.js path proved out, swept the office HTML
modules for dead code and stale documentation.
**Unused `_internal` exports removed (`html.ts`):**
- `renderMammothBody` — only called within the file (by
`wordDocToHtmlViaMammoth` and `wordDocToHtml`), never imported by
tests.
- `DOCX_PREVIEW_CDN` — internal config constant, never referenced.
- `PPTX_PREVIEW_CDN` — same, never referenced.
The remaining `_internal` surface (`wordDocToHtmlViaCdn`,
`wordDocToHtmlViaMammoth`, `pptxToHtmlViaCdn`,
`MAX_DOCX_CDN_BINARY_BYTES`, `MAX_PPTX_CDN_BINARY_BYTES`,
`OFFICE_HTML_OUTPUT_CAP`) is all actively used by the spec file.
**Stale JSDoc fixed (`libreoffice.ts`):**
Module-level header still claimed we "embed the PDF as a base64
data:application/pdf URI" and "rely on the host browser's built-in
PDF viewer". Both untrue after the pdf.js switch in commit b2cc81ad8.
Updated to:
- Describe the actual pipeline: PPTX → soffice → PDF → pdf.js → canvas
- Document the dead-end iterations (data: blocked, blob: also blocked,
pdf.js works) so future readers don't re-discover the same Chrome
PDF-viewer-in-sandboxed-iframe limitation
- Drop "(POC)" tag — the path is production-quality, just opt-in
- Adjust disk footprint estimate (250-350 MB with
`--no-install-recommends` is more accurate than the 500 MB original)
No production code changes; tests still 505 passing.
* ✨ feat: per-format LibreOffice opt-in (env value accepts format list)
Manual e2e on PR #12934: enabling `OFFICE_PREVIEW_LIBREOFFICE=true`
forces both DOCX and PPTX through the LibreOffice path. DOCX renders
~instantly via docx-preview and rarely needs the LibreOffice
treatment; paying the ~2-3 s cold-start there hurts UX without
adding much.
Solution: extend the env var to accept three forms:
- Truthy (`true`/`1`/`yes`): all formats — backwards compatible
with the previous behavior
- Falsy (`false`/`0`/`no`/empty/unset): no formats — default
- Comma-separated list (`pptx`, `pptx,docx`): just those formats
Practical guidance documented in the module header: most operators
will set `OFFICE_PREVIEW_LIBREOFFICE=pptx` — pptx-preview chokes on
pptxgenjs decks and the slide-list fallback loses formatting, so
LibreOffice is the only path that produces a faithful PPTX preview.
DOCX is well-served by docx-preview's existing CDN renderer.
API:
- New `isLibreOfficeEnabledFor(format)` is the per-format gate, used
by `tryLibreOfficePreview` to short-circuit before doing work.
- Existing `isLibreOfficeEnabled()` retained for "any format
enabled" diagnostic checks (returns true if at least one format
is opted in).
- Internal `parseLibreOfficeEnablement` returns `'all' | Set | null`
— keeps the gate future-proof: adding a new format to the
LibreOffice route doesnt require operators to re-enumerate their
env value.
Edge cases handled:
- Whitespace-tolerant: ` pptx , docx ` works
- Case-insensitive on both env value AND format name
- Empty list entries dropped: `pptx, ,docx` enables pptx + docx
- Empty string treated as unset (not as a valid empty list)
Tests: 21 new cases pinning the parse semantics + per-format gate
(`pptx` env vs `docx` lookup → false, etc.). Existing
`isLibreOfficeEnabled` tests retained but renamed to clarify the
"any format" semantic.
Total file tests: 526 passing (+21 vs before).
* 🔒 fix: officeHtmlBucket only does MIME fallback when extension is empty
Codex P2 review on PR #12934: the server's `officeHtmlBucket` falls
back to MIME whenever the extension isn't an OFFICE extension. The
client's `detectArtifactTypeFromFile` is stricter — it routes by
extension first for ANY known extension (`.txt` → PLAIN_TEXT,
`.md` → MARKDOWN, `.py` → CODE, etc.), only falling back to MIME
when the extension is unknown.
Mismatch case: `notes.txt` shipped with `Content-Type: application/
vnd.openxmlformats-officedocument.wordprocessingml.document`. Server
runs `officeHtmlBucket` → extension `.txt` not office → MIME fallback
→ 'docx' → produces full HTML, sets `textFormat: 'html'`. Client
routes by extension to PLAIN_TEXT (extension wins), markdown viewer
escapes the HTML, user sees raw `<html>...` markup instead of the
rendered preview.
Fix: server only falls back to MIME when extension is genuinely empty
(extensionless filename). Symmetric with the client's "extension
wins for any known extension" semantic — neither will mis-route.
Trade-off: a true DOCX renamed to `myfile.bin` with the canonical
DOCX MIME no longer routes through office HTML on the server. The
client would have routed to the office bucket via MIME, then the
security gate (`textFormat !== 'html'`) would have downgraded to
PLAIN_TEXT anyway. So the user-visible outcome is the same (raw
bytes via PLAIN_TEXT) — the new behavior just avoids producing HTML
that the client would never use.
Long-term fix: share the extension routing table in data-provider
so both server and client query the same source of truth. Out of
scope for this PR.
Tests: new 8-case `it.each` block in `officeHtmlBucket predicate`
locks in the contract — `.txt`/`.md`/`.json`/`.py`/`.html`/`.css`
+ office MIME → null, and `.bin`/`.dat` + office MIME → null too.
Existing extension-wins tests still pass unchanged.
Total file tests: 534 (+8 vs before).
Two `dangerouslySetInnerHTML` sites rendered admin-supplied HTML
without sanitization:
- `Banner.tsx` rendered `banner.message` directly.
- `MCPConfigDialog.tsx` rendered each `customUserVars` description.
Wrap both with DOMPurify, allowing only the inline tags needed for
formatting (links, emphasis, line breaks). Hardens against compromised
admin or yaml supply-chain scenarios. Pattern matches the existing
`CustomUserVarsSection.tsx` and `Tooltip.tsx` sanitizer setup.
`/c/new?prompt=…&submit=true` previously auto-submitted the prompt
unconditionally. For deployments where users may receive crafted
links from external sources, an authenticated victim's click can
trigger an immediate, attacker-controlled prompt against a memory- or
tool-enabled model — providing a 1-click vector for prompt-injection
exfiltration via markdown image rendering.
Add `interface.autoSubmitFromUrl` (default `true` to preserve current
behavior). Operators handling sensitive memory/tool data can set it
to `false` so URL-supplied prompts only pre-fill the composer; the
user must press Send explicitly.
`resizeAvatar` previously called `node-fetch` on any string input with
no validation. When OIDC providers surface a user-controllable
`picture` claim, this could be used to make blind SSRF requests to
internal services on every social login.
Wrap the URL fetch with:
- An allowlist on the URL protocol (http/https only).
- The shared `createSSRFSafeAgents` utility, which blocks resolution to
private, loopback, and link-local IPs at TCP connect time
(TOCTOU-safe; works equally for hostname targets that DNS-resolve
privately and for IP-literal targets, since Node's `net.Socket`
always dispatches through the agent's `lookup` hook).
- `redirect: 'error'` so a public-IP redirect target cannot be used to
bypass the agent check on a subsequent hop.
- A 5-second total request budget (node-fetch v2's `timeout` covers
request initiation through full body receipt, bounding slow-loris
exposure rather than just the TCP connect).
- A 10 MB response cap (`size` option + `Content-Length` pre-check +
post-read length assertion) so a hostile payload cannot exhaust
memory before `sharp()` rejects it.
Fetch the canonicalized `parsed.href` rather than the raw input string
to eliminate any future parser-differential between `new URL()` and
the underlying fetch implementation.
Per-call agent construction is intentional: the avatar path runs once
per social login per user, so pooling adds complexity without a
measurable benefit. Documented inline.
Comprehensive test coverage in `avatar.spec.js`:
- Rejects malformed URLs, non-http(s) schemes (file://, data:,
javascript:).
- Asserts the happy-path canonicalization (`fetch` is called with
`parsed.href`) and the SSRF-safe agent factory routing
(https→httpsAgent, http→httpAgent).
- Rejects non-2xx HTTP status.
- Rejects an oversized Content-Length before reading the body, and
asserts `.buffer()` is never invoked in that case.
- Rejects an oversized body even when the server lies about / omits
Content-Length.
- Surfaces ESSRF, redirect, and `size` overflow errors thrown by the
fetch layer.
- Confirms Buffer inputs bypass the fetcher entirely.
* 🪟 feat: Add allowedAddresses Exemption List For SSRF-Guarded Targets
LibreChat already blocks SSRF-prone targets (private IPs, loopback,
link-local, .internal/.local TLDs) at every server-side fetch site
that consumes user-controllable URLs — custom-endpoint baseURLs, MCP
servers, OpenAPI Actions, and OAuth endpoints. The only existing
escape hatch is `allowedDomains`, but that flips the field into a
strict whitelist: adding `127.0.0.1` to permit a self-hosted Ollama
also blocks every public destination that isn't in the list.
Introduce `allowedAddresses` as the orthogonal primitive: a private-
IP-space exemption list. When a hostname or its resolved IP appears
in the list, the SSRF block is bypassed for that target. Public
destinations remain reachable. Operators can now run self-hosted
LLMs / MCP servers / Action endpoints on private addresses without
weakening the default-deny posture for everything else.
Schema additions in `packages/data-provider/src/config.ts`:
- `endpoints.allowedAddresses` (new — gates `validateEndpointURL`)
- `mcpSettings.allowedAddresses` (parallel to `allowedDomains`)
- `actions.allowedAddresses` (parallel to `allowedDomains`)
Core changes in `packages/api/src/auth/`:
- New `isAddressAllowed(hostnameOrIP, allowedAddresses)` — pure,
case-insensitive, bracket-stripped literal match.
- Threaded the list through `isSSRFTarget`, `resolveHostnameSSRF`,
`isDomainAllowedCore`, `isActionDomainAllowed`, `isMCPDomainAllowed`,
`isOAuthUrlAllowed`, and `validateEndpointURL`.
- Extended `createSSRFSafeAgents` and `createSSRFSafeUndiciConnect`
to accept the list, building an SSRF-safe DNS lookup that exempts
matching hostnames/IPs at TCP connect time (TOCTOU-safe).
Wiring:
- Custom and OpenAI endpoint initialize sites pass
`endpoints.allowedAddresses` to `validateEndpointURL`.
- `MCPServersRegistry` stores `allowedAddresses` and exposes it via
`getAllowedAddresses()`. The factory, connection class, manager,
`UserConnectionManager`, and `ConnectionsRepository` all thread
it through to the SSRF utilities.
- `MCPOAuthHandler.initiateOAuthFlow`, `refreshOAuthTokens`, and
`validateOAuthUrl` accept the list and consult it on every URL
validation along the OAuth chain.
- `ToolService`, `ActionService`, and the assistants/agents action
routes pass `actions.allowedAddresses` to `isActionDomainAllowed`
and to `createSSRFSafeAgents` for runtime action calls.
- `initializeMCPs.js` reads `mcpSettings.allowedAddresses` from the
app config and forwards it to the registry constructor.
Documentation:
- `librechat.example.yaml` shows the new field next to each existing
`allowedDomains` block, with a note clarifying that
`allowedAddresses` is an exemption list (not a whitelist).
Tests:
- Unit tests for `isAddressAllowed` covering literal IPs, hostnames,
IPv6 brackets, case insensitivity, and partial-match rejection.
- Exemption tests for every entry point: `isSSRFTarget`,
`resolveHostnameSSRF`, `validateEndpointURL`, `isActionDomainAllowed`,
`isMCPDomainAllowed`, `isOAuthUrlAllowed`.
- Existing tests updated to reflect the new optional parameter.
Default behavior is unchanged: omitted = empty list = no exemptions.
* 🩹 fix: Plumb allowedAddresses Through AppConfig endpoints Type
The initial PR added `endpoints.allowedAddresses` to the
data-provider config schema and consumed it in the endpoint
initialize sites, but the runtime `AppConfig.endpoints` shape in
`@librechat/data-schemas` was a hand-maintained subset that didn't
include the new field — so `tsc` rejected `appConfig.endpoints.allowedAddresses`.
Add the field to `AppConfig['endpoints']` in
`packages/data-schemas/src/types/app.ts` and forward it from the
loaded config in `packages/data-schemas/src/app/endpoints.ts` so the
runtime config carries the value.
Update `initializeMCPs.spec.js` to expect the third positional
argument (`allowedAddresses`) on the `createMCPServersRegistry` call.
* 🩹 fix: Enforce allowedDomains Before allowedAddresses In isOAuthUrlAllowed
The initial implementation checked the address exemption first, so a
URL whose hostname appeared in `allowedAddresses` would return true
even when the admin had configured `allowedDomains` as a strict bound
on OAuth endpoints. A malicious MCP server could advertise OAuth
metadata, token, or revocation URLs at any address the admin had
permitted for an unrelated reason (a self-hosted LLM at `127.0.0.1`,
for example) and pass validation, expanding SSRF reach beyond the
configured domain whitelist.
Reorder: when `allowedDomains` is set, treat it as authoritative —
return true only if the URL matches a domain entry, otherwise fall
through to false. The address exemption only applies when no
`allowedDomains` is configured (mirrors how the downstream SSRF check
in `validateOAuthUrl` consults `allowedAddresses`).
Add a regression test asserting that an `allowedAddresses` entry does
not broaden a configured `allowedDomains` list.
Reported by chatgpt-codex-connector on PR #12933.
* 🩹 fix: Forward allowedAddresses To Remaining OAuth Callers
Two `MCPOAuthHandler` callers still used the pre-feature signatures and
were silently dropping the new `allowedAddresses` argument:
- `api/server/routes/mcp.js` invoked `initiateOAuthFlow` with the old
5-argument shape, so OAuth flows initiated through the route handler
ignored the registry's `getAllowedAddresses()` and would reject any
metadata/authorization/token URL on a permitted private host.
- `api/server/controllers/UserController.js#maybeUninstallOAuthMCP`
invoked `revokeOAuthToken` without the address exemption, so
uninstalling an OAuth-backed MCP server on a permitted private host
would fail at the revocation step even though the rest of the MCP
connection path now permits it.
Both sites now read `allowedAddresses` from the registry alongside
`allowedDomains` and forward it. Reported by Copilot on PR #12933.
* 🩹 fix: Update Test Mocks And Assertions For OAuth allowedAddresses
The previous commit started passing `allowedAddresses` to
`MCPOAuthHandler.initiateOAuthFlow` from `api/server/routes/mcp.js`
and to `MCPOAuthHandler.revokeOAuthToken` from
`api/server/controllers/UserController.js`, but the corresponding
test files mocked the registry without `getAllowedAddresses` (causing
`TypeError`s) and asserted the old positional shape on
`toHaveBeenCalledWith`.
Update the mocks and assertions to match the new arity:
- `api/server/routes/__tests__/mcp.spec.js`: add
`getAllowedDomains`/`getAllowedAddresses` to the registry mock and
expect the additional positional args on `initiateOAuthFlow`.
- `api/server/controllers/__tests__/maybeUninstallOAuthMCP.spec.js`:
add a `getAllowedAddresses` mock alongside the existing
`getAllowedDomains` and seed it in `setupOAuthServerFound`.
- `api/server/controllers/__tests__/UserController.mcpOAuth.spec.js`:
add `getAllowedAddresses` to the registry mock and expect the
trailing `null` arg on the three `revokeOAuthToken` assertions.
* 🛡️ fix: Address Comprehensive Review — Scope allowedAddresses To Private IP Space
Major findings from the comprehensive PR review (severity → fix):
**CRITICAL — `validateOAuthUrl` SSRF fallback bypass.** When `allowedDomains`
is configured and a URL fails the whitelist, the SSRF fallback in
`validateOAuthUrl` was still passing `allowedAddresses` to `isSSRFTarget` /
`resolveHostnameSSRF`, letting a malicious MCP server advertise OAuth
endpoints at any address the admin had permitted for an unrelated reason.
Suppress `allowedAddresses` in the fallback when `allowedDomains` is active —
the address exemption is opt-in for the no-whitelist mode only.
**MAJOR — WebSocket transport SSRF check ignored exemptions.** The
`constructTransport` WebSocket branch called `resolveHostnameSSRF(wsHostname)`
without `this.allowedAddresses`, so a permitted private MCP server would
pass `isMCPDomainAllowed` but be blocked at transport creation. Forward
the exemption.
**Scope `allowedAddresses` to private IP space only (operator directive).**
The exemption list is for permitting private/internal targets; it must not
be a back-door to broaden trust to public destinations.
- Schema (`packages/data-provider/src/config.ts`): new
`allowedAddressesSchema` rejects URLs (`://`), paths/CIDR (`/`),
whitespace, and public IPv4/IPv6 literals at config-load time. Wired
into `endpoints`, `mcpSettings`, and `actions`.
- Runtime (`packages/api/src/auth/domain.ts`): `isAddressAllowed` now
drops public-IP candidates and public-IP entries on the match path —
defense in depth so a misconfigured runtime list never grants exemption.
- Hot path (`packages/api/src/auth/agent.ts`): `buildSSRFSafeLookup`
pre-normalizes the list into a `Set<string>` once at construction and
applies the same scoping filter, so the connect-time DNS lookup is an
O(1) Set membership check instead of a full re-iterate-and-normalize on
every outbound request.
**Test coverage for the connect-time and OAuth-fallback paths.**
- `agent.spec.ts`: new describe block exercising `buildSSRFSafeLookup` and
`createSSRFSafe*` with `allowedAddresses` — hostname-literal exemption,
resolved-IP exemption, public-IP scoping, URL/CIDR/whitespace rejection,
and the default no-list block.
- `handler.allowedAddresses.test.ts` (new): integration tests for
`validateOAuthUrl` — covers both the no-domains-set "permit private"
path and the strict-bound regression where `allowedAddresses` must NOT
bypass `allowedDomains`.
**Documentation & cleanup.**
- `connection.ts` redirect SSRF check: explicit comment that
`allowedAddresses` is intentionally NOT consulted for redirect targets
(server-controlled, must not inherit the admin's exemption).
- `MCPConnectionFactory.test.ts`: replaced an `eslint-disable` with a
proper `import { getTenantId } from '@librechat/data-schemas'`. The
disable was added to make a pre-existing `require()` quiet — the cleaner
fix is to use the existing top-level import.
Updated `MCPConnectionSSRF.test.ts` WebSocket SSRF assertions to match the
new two-argument call shape (`hostname, allowedAddresses`).
* 🩹 fix: Require Absolute URL Before allowedAddresses Trust Bypass In isOAuthUrlAllowed
`parseDomainSpec` is lenient — it silently prepends `https://` to
schemeless inputs so it can match patterns like bare `example.com`.
That leniency leaked into `isOAuthUrlAllowed`'s new
`allowedAddresses` short-circuit: a value like `10.0.0.5/oauth` (no
scheme) would parse successfully via the prepended default, hit the
address-exemption path, return `true`, and skip `validateOAuthUrl`'s
strict `new URL(url)` parse-or-throw — only to fail later in OAuth
discovery with a less clear runtime error.
Add a strict `new URL(url)` gate at the top of `isOAuthUrlAllowed`.
Schemeless inputs now fall through to `validateOAuthUrl`'s explicit
"Invalid OAuth <field>" rejection. Tests added in both
`auth/domain.spec.ts` (unit) and the OAuth handler integration spec
(end-to-end).
Reported by chatgpt-codex-connector (P2) on PR #12933.
* 🛡️ fix: Address Follow-Up Comprehensive Review — Schema Tests, Shared Normalization, host:port
Auditing the second comprehensive review:
**F1 MAJOR — schema validation untested.** `allowedAddressesSchema` had
zero coverage, so a regression in the three refinement stages or the
three wiring locations (`endpoints` / `mcpSettings` / `actions`) would
silently let invalid entries reach the runtime. Added a dedicated
`describe('allowedAddressesSchema')` block in `config.spec.ts` covering:
valid private IPs (v4 + v6, including the previously-missed 192.0.0.0/24
range), accepted hostnames, all rejection categories (URLs, CIDR, paths,
whitespace tabs/newlines, host:port, public IP literals), and full
`configSchema.parse()` integration at each of the three nesting points.
**F2 MINOR — `isPrivateIPv4Literal` divergence.** The schema reimpl in
`packages/data-provider` was discarding the `c` octet, so the
`192.0.0.0/24` (RFC 5736 IETF protocol assignments) range that the
authoritative `isPrivateIPv4` accepts was being rejected with a
misleading "public IP" error. Destructure `c` and add the missing range
check; covered by the new schema tests.
**F3 MINOR — DRY violation across `domain.ts` and `agent.ts`.** Both
files had independent normalization implementations with a subtle
whitespace-check divergence (`/\s/` vs `.includes(' ')`). Extracted the
shared logic into a new `packages/api/src/auth/allowedAddresses.ts`
module that both consumers import:
- `normalizeAddressEntry(entry)` — single-entry shape check
- `looksLikeHostPort(entry)` — host:port detector (used by F4)
- `normalizeAllowedAddressesSet(list)` — pre-normalized Set for the
connect-time hot path
- `isAddressInAllowedSet(candidate, set)` — membership check that
enforces private-IP scoping on the candidate
Both `isAddressAllowed` (preflight) and `buildSSRFSafeLookup` (connect)
now go through the same primitives; the whitespace divergence is gone.
To break the import cycle (`allowedAddresses` needs `isPrivateIP`,
`domain` previously owned it), extracted IP private-range detection
into a leaf `auth/ip.ts` module. `domain.ts` re-exports `isPrivateIP`
for backward compatibility with existing call sites.
**F4 MINOR — `host:port` silently misclassified.** Entries like
`localhost:8080` previously slipped through the URL/path guard, were
mis-detected as IPv6, failed `isPrivateIP`, and were silently dropped
with a misleading "public IP" schema error. Added an explicit
`looksLikeHostPort` check with a clear error: "allowedAddresses
entries must not include a port — list the bare hostname or IP only."
Bare `::1`, `[::1]`, and other valid IPv6 literals are intentionally
not matched (regex distinguishes by colon count and the bracketed
`[ipv6]:port` form).
**F5 MINOR — hostname-trust documentation gap.** Hostname entries
short-circuit `resolveHostnameSSRF` before any DNS lookup — that's a
deliberate design (admin trusts the name) but it means the exemption
follows whatever the name resolves to at runtime. Added an explicit
note in `librechat.example.yaml` for both `mcpSettings.allowedAddresses`
and `endpoints.allowedAddresses`: "a hostname entry trusts whatever IP
that name resolves to. Only list hostnames whose DNS you control.
Prefer literal IPs when you can."
**F6** (8 positional params) is flagged for follow-up; refactor to an
options object is a breaking-API change deferred to a separate PR.
**F7** (redirect/WebSocket asymmetry, NIT, conf 40) — skipping; the
existing inline comment is sufficient.
* 🧹 chore: Address Follow-Up NITs — Import Order And Mirror-Function Naming
Three NITs from the latest comprehensive review:
**NIT #1 (conf 85) — local import order.** AGENTS.md requires local
imports sorted longest-to-shortest. Both `domain.ts` and `agent.ts`
had `./ip` (shorter) before `./allowedAddresses` (longer). Swapped.
**NIT #2 (conf 60) — missing cross-reference.** The schema-side
`isHostPortShape` in `packages/data-provider/src/config.ts` had no
note pointing at the canonical runtime mirror. Added a JSDoc paragraph
explaining the mirror relationship and why a local copy exists (the
data-provider package can't import from `@librechat/api` without
creating a circular dependency).
**NIT #3 (conf 50) — naming inconsistency.** Renamed
`isHostPortShape` → `looksLikeHostPort` so the schema mirror matches
the runtime helper exactly. Kept as a separate function (not a shared
import) for the same circular-dependency reason; the matching name
makes it obvious they should stay in lockstep.
The Passport local strategy validation error logged the entire request
body (including the password) into error logs. Replace it with the
email only, matching the metadata shape used by sibling log calls in
the same function.
* 🔧 chore: Update dependencies in package-lock.json and package.json
- Bump version of @librechat/agents to 3.1.75-dev.0 in multiple package.json files.
- Upgrade various AWS SDK and Smithy dependencies to their latest versions in package-lock.json for improved stability and performance.
* 🔧 chore: Update AWS SDK and Smithy dependencies in package-lock.json
- Bump version of @aws-sdk/client-bedrock-runtime to 3.1041.0 and update related dependencies for improved performance and stability.
- Upgrade various AWS SDK and Smithy packages to their latest versions, ensuring compatibility and enhanced functionality.
* chore: Align LibreChat with agents LangChain upgrade
- Route LangChain imports through @librechat/agents facade exports
- Update @librechat/agents to 3.1.75-dev.1 and remove direct LangChain deps
- Normalize nullable agent model params and API key override typing
- Update Google thinking config typing for newer LangChain packages
- Refresh targeted audit-related dependency overrides
* chore: Add Jest types for API specs
* test: Fix LangChain upgrade CI specs
* test: Exercise agents env facade
* fix: Clean up TS preview diagnostics
* fix: Address Codex review feedback
Fixes #12912.\n\n- Clear stored MCP OAuth tokens and flow state on revoke cleanup-only paths.\n- Keep provider revocation best-effort when token and client metadata are available.\n- Add controller and function coverage for stale metadata, missing config, and cleanup failure paths.
* 🩹 fix: Sync ControlCombobox popover width with trigger after layout changes
The popover width was measured once on mount via offsetWidth. When the agent builder side panel opens after a page reload with the sidebar collapsed, the trigger button is initially measured during the layout transition (~26px) and never re-measured, leaving the agent select dropdown rendered at the far left with no options fully visible.
Use a ResizeObserver to keep buttonWidth in sync with the trigger's actual width whenever it resizes, then disconnect on unmount.
* test: cover ControlCombobox isCollapsed, no-ResizeObserver, and zero-width branches
Address review feedback:
- Use button.offsetWidth as the ResizeObserver fallback instead of
entry.contentRect.width to avoid a content-box vs border-box mismatch in
pre-2022 browsers that ship ResizeObserver without borderBoxSize.
- Add tests for the three previously-untested branches: isCollapsed=true
(no observation of the trigger), ResizeObserver unavailable (sync-only
measurement), and zero-width entries (state unchanged).
* test: lock the button.offsetWidth fallback against revert
Add a test that drives the ResizeObserver callback with borderBoxSize
absent and divergent contentRect.width vs offsetWidth (251 vs 275).
The fix would silently revert to entry.contentRect.width without this
test failing, so this pins the chosen fallback semantics.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 📥 fix: Use Endpoint-Aware Default Model on Imported Conversations
Claude conversations imported from claude.ai's data export display
"gpt-4o-mini" in the chat UI until the page is refreshed, and any
attempt to send a message before refreshing fails with "The model
'gpt-4o-mini' is not available for Anthropic."
Root cause: ImportBatchBuilder.finishConversation() unconditionally
defaulted the saved conversation's `model` field to
openAISettings.model.default, regardless of `this.endpoint`. Claude
exports don't carry a model name, so every imported Claude conversation
landed with endpoint=anthropic but model=gpt-4o-mini.
Fix: pick the default based on `this.endpoint` via a small lookup
(openAI -> gpt-4o-mini, anthropic -> claude-3-5-sonnet-latest), keeping
the existing OpenAI default as the fallback for unknown endpoints.
Fixes#12844
* 🪄 refactor: Resolve Import Default Model From `modelsConfig`
Replace the hardcoded per-endpoint default lookup added in the previous
commit with a runtime resolver that consults the same models config the
chat UI uses (`getModelsConfig` in ModelController -> `loadDefaultModels`
+ `loadConfigModels`). This way an imported conversation defaults to a
model the LibreChat instance has actually configured / discovered for
the endpoint, instead of a hardcoded constant that may not exist on this
deployment.
Resolution order:
1. First non-empty model in `modelsConfig[endpoint]`.
2. Per-endpoint hardcoded fallback (anthropic/openAI settings) if the
runtime config is empty for the endpoint or `getModelsConfig` throws.
3. `openAISettings.model.default` if even the per-endpoint fallback is
missing (unknown endpoint).
`importBatchBuilder.finishConversation` now accepts an optional
`defaultModel` argument; each importer resolves it once at the top via
`resolveImportDefaultModel({ endpoint, requestUserId, userRole })` and
threads it through. ChatGPT message-level model selection also falls
back to the resolved default before the hardcoded gpt-4o-mini.
* 🔌 fix: Follow 307/308 redirects in MCP streamable HTTP transport
Some MCP servers (e.g. Coda) return 308 Permanent Redirect to route
doc-scoped tool calls to a different endpoint path. The fetch wrapper
used `redirect: 'manual'` for SSRF protection, which silently dropped
these redirects and caused tool calls to fail with empty error bodies.
Follow 307/308 redirects (method-preserving per RFC 7538) up to a
depth of 5. SSRF safety is preserved because the same undici Agent
with its SSRF-safe connect function validates redirect targets.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🛡️ fix: Harden MCP 307/308 redirect handling against SSRF and credential leaks
- Validate every redirect target against `resolveHostnameSSRF` so allowlist
deployments (which disable connect-time SSRF protection) still block hops to
private/reserved IPs.
- Strip `Authorization`, `Cookie`, `mcp-session-id`, and any user-injected
headers when a 307/308 crosses an origin boundary, mirroring browser/Fetch
behavior so a redirecting MCP server can't exfiltrate credentials.
- Cancel the intermediate response body before each next hop so undici can
reuse pooled sockets rather than holding them until GC.
- Restructure redirect test helpers to be same-origin (matching real-world
Coda-style routing), drop dead setup code, fix the misleading "5 hops
successfully" test, and add coverage for SSRF-blocked redirects, cross-
origin credential stripping, and same-origin credential preservation.
* 🛡️ fix: Also strip `serverConfig.headers` on cross-origin MCP redirects
Previously only runtime `setRequestHeaders` keys were treated as secret on a
307/308 cross-origin hop. API keys baked into `serverConfig.headers` (passed
through `requestInit.headers` at transport construction time) survived
stripping, so a malicious MCP endpoint could exfiltrate them by returning a
cross-origin `Location`. Pass the configured header keys through to
`createFetchFunction` so both runtime and config secrets are stripped.
The cross-origin credential test now also configures `serverConfig.headers`
to lock in this behavior.
* 🧹 chore: Tighten MCP redirect-stripping coverage and helper duplication
- Add `proxy-authorization` to the cross-origin forbidden header set so a
forward-proxy credential header would also be stripped on a cross-origin
hop, matching the Fetch-spec list.
- Strengthen the cross-origin credential test with positive assertions that
benign protocol headers (`accept`, `content-type`) survive the hop, so a
regression that strips everything indiscriminately would now fail.
- Extract the duplicated MCP request handler / session-teardown logic from
three test helpers into shared `createMCPRequestHandler` and
`closeMCPSessions` utilities.
* 🛠️ fix: Handle `Request` inputs in MCP `customFetch` URL derivation
`customFetch` is typed to accept `UndiciRequestInfo` (`string | URL | Request`),
but `Request.prototype.toString()` returns `"[object Request]"`. The previous
implementation derived `originalOrigin` and the redirect base via
`url.toString()`, so a `Request` input would throw inside `new URL(...)` before
any network call — a regression even when no redirect was involved.
Add a `getRequestUrlString` helper that extracts the URL string for all three
shapes, track the URL string alongside the fetch input through the redirect
loop, and add parameterized tests that exercise `customFetch` with each shape.
* 🛠️ fix: Don't override `Request` input headers in MCP `buildFetchInit`
Previously `buildFetchInit` always set `headers` on the returned init —
even when neither `init.headers` nor runtime headers contributed anything.
Passing `headers: {}` to `undiciFetch` overrides the headers carried on a
`Request` input (auth tokens, MCP session, protocol negotiation), so
Request-based wrappers could fail authentication even without a redirect
in play. Skip the `headers` override entirely when there is nothing to
merge.
Adds a regression test that supplies `Authorization` and a custom header
on the `Request` itself and asserts both reach the target server.
* 🛠️ fix: Preserve `Request` method/body across MCP redirects + guard cross-origin strip
Two regressions surfaced by extending `customFetch` to accept `Request` inputs:
1. **307/308 method/body loss.** The redirect loop switches `url` to the
new `Location` string, but the original `Request`'s method and body
stayed bound to the (now-discarded) `Request` object. A redirected
POST silently became a GET with no payload — the exact behavior the
method-preserving codes are designed to prevent. Added a
`resolveFetchInput` helper that runs once at the top of `customFetch`,
extracts a `Request`'s method/body/headers into the shared init, and
buffers the body via `arrayBuffer()` so 307/308 retries can replay it.
2. **Cross-origin strip crashed on absent headers.** After the previous
fix that stopped `buildFetchInit` from setting `headers: {}`,
`currentInit.headers` could legitimately be `undefined`. The
cross-origin branch read it as a `Record` and called `Object.entries`
on `undefined`, throwing `TypeError`. Guard the branch on
`currentInit.headers != null` — when there are no headers there is
nothing to strip.
Adds two regression tests: a POST-with-body `Request` that 308-redirects
cross-origin (asserts both method and body survive) and a no-headers
cross-origin redirect (asserts the strip path no longer crashes).
* 🛠️ fix: Forward `Request.signal` through MCP `customFetch` normalization
`resolveFetchInput` was copying method/body/headers off a `Request` input
but dropping `Request.signal` on the floor, so a caller that wired an
`AbortController` onto the `Request` for cancellation/timeouts lost that
wiring as soon as we re-shaped the input into the `(string, init)` pair
used by the redirect loop. Subsequent aborts no longer reached the
in-flight fetch — a regression from the pre-PR code, which forwarded
the original `Request` directly to undici.
Forward the signal alongside method/body/headers, with explicit
`init.signal` still winning per Fetch-spec semantics. Regression test
aborts a controller before calling \`customFetch\` with the wired
`Request` and asserts the call rejects.
* 🧪 test: Pin URL.origin contract for protocol-downgrade redirect handling
Audit follow-up. The cross-origin strip path keys off
`targetUrl.origin !== originalOrigin`, and `URL.origin` is defined as
`scheme + "://" + host + ":" + port`, so a same-host `https → http`
redirect produces a different origin and trips the strip path through
the existing logic — no separate code path needed.
Pin that contract with a small unit test so a future change to URL
semantics (or a refactor that swaps in a different comparison) doesn't
silently regress protocol-downgrade stripping. Standing up a TLS
fixture (self-signed cert, undici skip-verify, etc.) just to re-prove
the URL spec is wasted complexity.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🧹 chore: Strip code-execution boilerplate from tool output
The bash executor in `@librechat/agents` appends two kinds of noise to
every successful run:
1. Trailing `Note:` paragraphs — long behavioral hints repeating
rules already in the system prompt ("Files from previous executions
are automatically available...", "Files in 'Available files' are
inputs..."). Re-stating these on every tool call adds ~50 tokens of
waste per call, which compounds across long agent traces.
2. Per-file `| <annotation>` suffixes on every line of `Generated
files:` / `Available files (...):`. The two section headers already
convey the new-vs-known distinction; the per-file annotations are
redundant *and* phrased inconsistently ("downloaded by the user"
vs. "displayed to the user" vs. "known to the user").
Strip both in a small `cleanCodeToolOutput` helper invoked from
`packages/api/src/agents/handlers.ts` for every tool listed in
`CODE_EXECUTION_TOOLS`. Non-code-execution tools pass through
unchanged. The cleaning happens *after* tool resolution but *before*
downstream consumers (model context, SSE forwarding, persistence) see
the content, so subsequent model turns get the lean output.
* 🩹 fix: Polish code-execution attachment rendering
Three rough edges visible in code-interpreter conversations:
1. **Sandbox-internal `.dirkeep` placeholders leak as file chips.** The
bash executor creates `.dirkeep` inside any new directory so the
stateless container preserves the folder across executions. After
`sanitizeArtifactPath`'s `_` prefix and 6-hex collision suffix it
surfaces as `_.dirkeep-<hash>` — a 0-byte chip with no value to the
user, sometimes hiding the real artifact behind it. New
`isInternalSandboxArtifact` helper filters them out of every
routing path (`Attachment`, `AttachmentGroup`, `LogContent`).
2. **The `-<hash>` collision suffix is visible in chip labels.** The
suffix is collision-avoidance machinery; users only need to see the
canonical name. New `displayFilename` strips it for display while
leaving the on-disk `attachment.filename` untouched so downloads
resolve. Applied across `FileContainer`, `ToolArtifactCard`,
`ToolMermaidArtifact`, and `LogContent`'s text-attachment label
path.
3. **0-byte / placeholder files outrank real artifacts in render
order.** Bucket sort by salience (non-empty before empty) sinks
stragglers to the bottom. Stable sort preserves arrival order for
peers.
Added regression tests cover the new helpers, the dirkeep filter
across buckets, and the within-bucket salience ordering.
* 🩹 fix: Don't auto-open artifact panel on history navigation
Navigating to a previous conversation full of code-execution artifacts
would auto-open the side panel and focus the most-recent artifact —
the same code path that fires for fresh streaming artifacts. Users
expect that "auto-open" behavior only when an artifact arrives via
SSE, not when they revisit an old chat.
Two-part gate:
1. `ToolArtifactCard`'s focus effect captures `isSubmitting` at first
render via a ref. A card mounted *during* a stream means a new
artifact arrived → steal panel focus (legacy behavior). A card
mounted while `isSubmitting === false` is part of conversation
history → leave focus alone.
2. `Presentation`'s panel-render condition gains `currentArtifactId
!= null`. With (1) keeping `currentArtifactId` null on history
load, the panel stops rendering at all on navigation — even if
`artifactsVisibility` was left `true` by a prior conversation.
User clicks on a chip to re-open (the click handler is unchanged
and unconditional).
Test seeds `isSubmittingFamily(0)` per case: existing tests opt into
streaming (default `true`) so legacy auto-focus assertions still hold;
new tests for history-load opt into `streaming: false` and verify
no auto-focus + click-to-open still works.
* 🩹 fix: Force panel visible on streaming artifact arrival
The previous commit gated `setCurrentArtifactId` on `isSubmitting` but
left `artifactsVisibility` untouched. When a user had explicitly
closed the panel earlier in the session, a fresh SSE artifact would
set `currentArtifactId` (so the chip read "click to close") but
`Presentation`'s render condition still required `visibility === true`
— net effect: the card claimed to be open, the panel stayed hidden.
Streaming arrivals now also call `setVisible(true)`, which is the
explicit "auto-open when first created" behavior the user asked for.
History mounts (`isSubmitting === false`) still leave both focus and
visibility alone, so navigating to an old conversation does not
re-open the panel.
Two regression tests added: one asserts streaming flips visibility on
even when seeded false, the other asserts history mounts leave a
seeded-false visibility alone.
* 🧹 chore: Tighten code-execution attachment polish per audit feedback
Resolves the eight actionable findings from the comprehensive audit:
- Scope `displayFilename` out of `FileContainer`: opt-in via a new
`displayName` prop. User-uploaded chips (input area, persisted
message files) keep their raw filename, eliminating the false-positive
class where `report-abc123.pdf` was silently rewritten to `report.pdf`.
Code-execution artifact paths in `Attachment.tsx` explicitly compute
the de-suffixed name and pass it through.
- Tighten `TRAILING_NOTES_PATTERN` to anchor on the two known boilerplate
openings (`Files from previous executions`, `Files in "Available files"`),
so a user-authored `Note:` line preceded by a blank line in stdout no
longer gets eaten along with everything after it.
- `ToolMermaidArtifact`: compute `visibleFilename` once and reuse for
title, content, and the download `aria-label` (was using the raw
`attachment.filename` for the aria-label, creating a screen-reader
inconsistency).
- `ToolArtifactCard`: read `isSubmittingFamily(0)` once via a
non-subscribing `useRecoilCallback`, instead of subscribing for the
full lifetime to a value the ref only ever needs at first render.
- Extract `bySalience` and `byEntrySalience` comparators from
`attachmentTypes.ts`, replacing the ten duplicated sort lambdas in
`Attachment.tsx` and `LogContent.tsx`.
- Treat `attachmentSalience({ bytes: undefined })` as neutral (`0`)
rather than empty (`1`); only an explicit `bytes === 0` sinks. Stops
non-code-exec sources (web-search inline results, files where the
schema omits the byte count) from silently sinking past real content.
- Pin the click-history test to the panel-open button by name instead
of relying on `getByRole('button', { pressed: false })`, which
matched by DOM order.
- Add the missing blank line between adjacent `it(...)` blocks.
- Drop the verbose narrating comments in `FileContainer` along with the
removed `displayFilename` import.
Adds three regression tests for the new behavior (FileContainer raw
filename, artifact-context displayName flow, user-authored `Note:` line
preserved through cleanup) and updates the salience test for the new
neutral-undefined semantics.
* 🧹 chore: Drop redundant `@testing-library/jest-dom` import in FileContainer spec
`client/test/setupTests.js` already imports the matchers globally for every
Jest test in the client workspace, so the explicit import here was dead code.
Removing it brings the spec in line with the broader convention used by
`ArtifactRouting.test.tsx`, `LogContent.test.tsx`, and `attachmentTypes.test.ts`.
* 🛡️ fix: Narrow `.dirkeep`/`.gitkeep` filter to the sandbox-specific form
`isInternalSandboxArtifact` was filtering bare `.dirkeep` / `.gitkeep`
along with the post-sanitization form. Bare versions never originate
from the bash executor (the dotfile rewrite + disambiguator step in
`sanitizeArtifactPath` always produces `_.dirkeep-<6 hex>`), so the only
real-world source of a bare `.gitkeep` is project scaffolding the user
uploaded — silently hiding it from every attachment bucket meant the
file disappeared with no way to surface or download it.
Tightening to `^_\.(?:dirkeep|gitkeep)-[0-9a-f]{6}$` keeps the
sandbox-placeholder filter intact while letting user-uploaded markers
render normally. Tests inverted accordingly: bare forms now expected to
render; only the post-sanitization form is filtered.
* 🩹 fix: Address comprehensive-review findings on attachment helpers
Five findings from the latest pass:
- **MAJOR — `displayFilename` false-positive on extensionless 6-hex.**
The previous regex `/-[0-9a-f]{6}(?=\.[^.]+$|$)/` stripped any leaf
ending in `-XXXXXX` regardless of context, so a user-named
`build-a1b2c3` (script-emitted hash artifact, no extension) lost its
tail and rendered as `build`. Split into two narrower patterns:
`COLLISION_SUFFIX_BEFORE_EXT` only matches when followed by an
extension; `SANITIZED_DOTFILE_TRAILING_SUFFIX` only fires when the
leaf starts with `_.` AND ends with `-XXXXXX` — the unambiguous
fingerprint of `sanitizeArtifactPath`'s dotfile rewrite.
- **MINOR — `isInternalSandboxArtifact` filter too aggressive.**
`(file.bytes ?? 0) > 0` treated undefined bytes as zero, falling
through to the regex check. Tightened to `file.bytes !== 0`: only
an *explicit* zero counts as the empty-placeholder shape worth
hiding. Non-code-exec sources without `bytes` populated render
normally now.
- **MINOR — `getValue()` could throw on a degenerate atom state.**
Switched the snapshot read in `ToolArtifactCard` to
`valueMaybe() ?? false` so a transient error / loading state on the
upstream selector doesn't crash card mount. The `false` default is
the right history-fallback (don't auto-open if we can't classify).
- **NIT — `attachmentSalience` / `bySalience` over-broad signature.**
Removed the test-only `{ bytes?: number }` arm; functions now accept
`TAttachment` directly. The internal `bytes` read still goes through
a cast since not every TAttachment branch declares it. Tests updated
to use the existing `baseAttachment(...)` helper.
- **MINOR — Missing regression test for extensionless 6-hex.**
Added `'build-a1b2c3'` and `'out/blob-deadbe'` cases that pin the
preservation behavior, plus an `isInternalSandboxArtifact` test that
asserts undefined-bytes attachments are not filtered.
* 🩹 fix: Make code-file artifacts click-to-open only
Removes mount-time auto-open from `ToolArtifactCard`. Streaming
arrivals no longer hijack the panel — even a freshly-emitted SSE
artifact registers silently in `artifactsState` and waits for the
user to click. Combined with `Presentation`'s
`currentArtifactId != null` render gate, the panel stays closed
across history navigation, page reload, and SSE arrival.
Click is the only path that opens the panel. `handleOpen` is
unchanged: first click focuses + reveals, second click on the same
chip closes.
Dropped:
- `useRecoilCallback` snapshot read of `isSubmittingFamily(0)`
- `mountedDuringStreamRef` ref + lazy-init block
- The whole focus + visibility effect (was effect 3)
- `useRef` import (now unused)
Tests:
- `ArtifactRouting.test.tsx` rewritten to exercise the click path:
registers-on-mount-without-focus, click-to-open-then-close, multi-
card-no-auto-focus, click-when-visibility-was-false. The streaming
state is no longer seeded; both `renderWith` and `renderWithProbe`
collapsed back to plain `RecoilRoot`.
- `LogContent.test.tsx` flips its panel-routing assertions from
`pressed: true` (which asserted auto-focus) to `pressed: false`
with a chip-title check (which asserts the panel card rendered
but stayed unfocused).
* Revert "🩹 fix: Make code-file artifacts click-to-open only"
This reverts commit 67615312878d58da1f9ea3cd40da5ea454d9e699.
* 🩹 fix: Exclude CODE bucket from streaming auto-open
Narrows the previous-commit revert: rich-preview artifacts (HTML,
React, Markdown, plain text) keep the legacy SSE auto-open UX, but
the CODE bucket (`.py`, `.js`, `.cpp`, `Dockerfile`, `Makefile`, …)
stays click-to-open even on streaming.
Source-code artifacts are typically supporting helpers the agent
emits alongside a richer deliverable (a Python script that builds
the actual `.html` output, for example). Auto-opening every
helper's panel each time it gets written would shove the panel
in front of the user every tool call. The user explicitly opens
a code chip when they want to inspect it.
Implementation:
- Focus+open effect skips early when `artifact.type === CODE`.
- `artifact.type` added to the dep array so the gate re-evaluates
if the type ever changes (it shouldn't, but the dep is honest).
- JSDoc updated to call out the carve-out.
Tests:
- New `does NOT auto-open a streaming CODE artifact (test.py is
click-to-open)` — seeds isSubmitting=true, mounts a `.py`,
asserts the artifact registers but currentArtifactId stays null.
- New `clicking a CODE artifact focuses it even though it skipped
auto-open` — confirms the click path still surfaces a `.py`.
- All 25 prior auto-open tests for HTML/React/Markdown/plain-text
buckets still pass unchanged: those types continue to auto-open
on streaming.
* 🧹 chore: Address two NITs from the audit-fix follow-up review
- **NIT #1 (conf 60)**: Add a test for the dotfile-with-extension
intersection (`_.config-abcdef.txt` → `.config.txt`). Both halves
of the path were tested separately — extension-anchored suffix
stripping and `_.` underscore restoration — but the combination
wasn't pinned. Adds `expect(displayFilename('_.config-abcdef.txt'))
.toBe('.config.txt')`.
- **NIT #2 (conf 25)**: Tighten the cast in `attachmentSalience` from
the anonymous `{ bytes?: number }` shape to the concrete
`TFile & TAttachmentMetadata` (the actual TAttachment branch that
declares `bytes`). Same runtime behavior; a future retype of
`TFile.bytes` will now surface here at compile time instead of
being silently papered over.
* 🩹 fix: Stop stripping `-<6 hex>` suffixes from non-dotfile filenames
Codex's repeated P2 was correct: the `COLLISION_SUFFIX_BEFORE_EXT`
regex stripped any `-<6 hex>` immediately before an extension
regardless of context. That collapsed legitimate user-named files
like `report-deadbe.csv` and `report-beef01.csv` onto the same chip
label `report.csv`, silently merging distinct files in the UI.
The structural truth: only the dotfile shape (`_.foo-XXXXXX`) carries
an unambiguous discriminator (the leading `_.` that
`sanitizeArtifactPath` adds when rewriting a leading dot). The
extension-only case (`name-<hash>.ext`) has no such discriminator —
we can't distinguish a sanitized `report 1.csv` (which became
`report_1-<hash>.csv`) from a user-named `report-deadbe.csv` from
the filename alone.
Recovering the non-dotfile case cleanly would require a backend
`wasSanitized` metadata flag we don't have. Without it, the safer
choice is to leave non-dotfile names alone — uglier when the file
*was* sanitized, but never collapses distinct files onto a shared
label.
Changes:
- Drop `COLLISION_SUFFIX_BEFORE_EXT`. Replace
`SANITIZED_DOTFILE_TRAILING_SUFFIX` with a unified
`SANITIZED_DOTFILE_PATTERN` that handles both extensionless and
with-extension dotfile shapes in one regex.
- Simplify `displayFilename` to a single match + reconstruct path.
- Update tests: drop the broad-stripping assertion
(`output-deadbe.csv` → `output.csv`), add explicit codex-regression
cases (`report-deadbe.csv` and `report-beef01.csv` preserve
unchanged), document the deliberate non-recovery for sanitized
non-dotfiles, update the AttachmentGroup→FileContainer integration
test to reflect the narrower stripping (non-dotfile `archive-deadbe.zip`
passes through; new dotfile `_.config-abcdef.zip` → `.config.zip`
exercises the recoverable path).
* 🩹 fix: Scope code-tool annotation stripping to file-list sections
Codex was right: the previous global `.replace` would mutate any line
ending in one of the three annotation phrases — even legitimate
stdout. A user script doing
`echo "foo | File is already downloaded by the user"` had its output
silently scrubbed before being fed back into model context.
New `FILE_SECTION_PATTERN` captures `Generated files:` /
`Available files (...)` blocks (header + lines starting with `- /`).
Annotation stripping now only runs *within* the captured file-list
section via a nested `.replace`, so:
- Inside the section: per-file `| <ann>` suffixes still get stripped
(line-per-file ≥ 4 files form, inline `, ` comma-separated ≤ 3
files form — both already covered by existing patterns).
- Outside the section: stdout, stderr, blank lines, the trailing
`Note:` paragraphs (handled by their own pattern), and any user
text that coincidentally contains an annotation phrase pass
through unchanged.
Tests:
- New `does NOT mutate stdout that legitimately contains an
annotation phrase outside a file-list section` pins the codex
regression: three coincidental phrases in stdout, no
`Generated files:` header, all three preserved verbatim.
- New `strips annotations inside a file-list section but preserves
identical phrases in stdout above it` covers the mixed case where
the same phrase appears in both stdout and a file listing —
stdout survives, listing gets cleaned, exactly one occurrence
remains.
- All 9 prior tests still pass (file-section stripping behavior
unchanged for both line-per-file and inline-comma layouts).
* fix(sse): treat responseCode===0 as transport failure, not server error
When a long-running model response (e.g. gpt-5.4 with web_search:true)
takes longer than the browser's idle connection timeout, the SSE transport
drops and sse.js fires an error event with responseCode=0 and e.data set
to the raw response buffer (non-JSON SSE text).
The previous guard `!responseCode` is truthy for both 0 (transport drop)
and undefined (genuine server-sent error event), so the client incorrectly
entered the server-error branch, tried to JSON.parse raw SSE text, logged
"Failed to parse server error", and showed the user a red error banner --
even though the backend continued processing and delivered the final answer
seconds later.
Fix 1 (client): change guard from `!responseCode` to `responseCode == null`
so that only undefined/null (no HTTP status at all) triggers the server-error
parse path. responseCode===0 now correctly falls through to the reconnect path.
Fix 2 (backend): after res.flushHeaders() the response is already committed
as SSE. The fallback branch that wrote res.status(404).json() was an HTTP/SSE
protocol violation. Replace with an SSE-conformant event:error frame + res.end().
* fix(sse): use onError helper on subscribe failure + add regression tests
Replace silent res.end() with onError('Failed to subscribe to stream')
so the client receives a parseable SSE error event instead of a stream
that closes with no signal. The previous res.end() left the UI stuck
in "submitting" state because no error/abort/final event ever fired.
Also adds two missing test cases for the responseCode guard change:
- responseCode === 0 with raw SSE buffer data must NOT call errorHandler
(transport failure should reconnect, not display garbage)
- responseCode == null with JSON error data MUST call errorHandler
(server-sent error events should still surface to the user)
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
Most of the codebase already supports the concept of *not* using a reranker w/
web search, except there was no way to initially setup an absent reranker
component.
Now there's a special path for skipping the reranker auth when loading web
search config, which allows for skipping the reranker when using web search.
* fix: graceful MCP OAuth revoke cleanup when tokens are missing (#12754)
`maybeUninstallOAuthMCP` in `api/server/controllers/UserController.js`
aborts before the DB-delete and flow-state cleanup steps whenever
`MCPTokenStorage.getTokens` throws `ReauthenticationRequiredError` —
which is exactly what happens when a user clicks "Revoke" on an MCP
server whose backend is already dead and whose refresh token is gone.
The resulting error is both surfaced to the log as a red line and, more
importantly, leaks the DB token row and OAuth flow state.
Wrap the token retrieval in try/catch following the same best-effort
pattern already used for the two `revokeOAuthToken` calls. On
`ReauthenticationRequiredError`, skip revocation silently (info log)
and continue to the cleanup steps. On any other unexpected error, log
a warning and continue — cleanup must always run.
Exported `maybeUninstallOAuthMCP` for direct unit testing and added
`api/server/controllers/__tests__/maybeUninstallOAuthMCP.spec.js` with
8 cases: early-return guards (non-MCP key, non-OAuth server, missing
client info), happy path (both tokens revoked + cleanup), both
failure-to-retrieve paths (ReauthenticationRequiredError and arbitrary
error — cleanup still runs in both), single-token path, and
revocation-call failures (cleanup still runs).
Fixes#12754.
* test: use instanceof check against real ReauthenticationRequiredError
Follow-up to the previous commit on this branch. Two changes:
1. `UserController.maybeUninstallOAuthMCP` now checks
`error instanceof ReauthenticationRequiredError` using the real
class imported from `@librechat/api`, instead of comparing
`error?.name === 'ReauthenticationRequiredError'`. The name-string
check matched any unrelated error that happened to have the same
`.name`; the `instanceof` check is a proper identity test.
2. The accompanying spec's jest mock for `@librechat/api` now
exposes a `ReauthenticationRequiredError` class, and the test
imports it from that mock so the `instanceof` comparison in the
production code holds during the test. Without this, the two
"skips revocation ... still runs cleanup" tests threw
`TypeError: Right-hand side of 'instanceof' is not an object`
because the mock left the class undefined.
All 8 tests in the spec pass.
When GOOGLE_KEY=user_provided is set as an endpoint config, the
loadAuthValues() function in credentials.js would pass the literal
string 'user_provided' to tools via the || fallback chain. This caused
Gemini Image Tools to fail at runtime with an invalid API key error,
as initializeGeminiClient() received the sentinel value instead of a
real key.
The fix aligns loadAuthValues() with checkPluginAuth() in format.ts,
which already correctly excludes user_provided and empty/whitespace
values. Now loadAuthValues() skips these values and continues to the
next field in the fallback chain or falls through to user DB values.
Added regression tests covering:
- user_provided sentinel is skipped, DB value used instead
- Fallback chain continues past user_provided to next field
- Empty and whitespace env values are skipped
- Real env values are returned correctly
- Optional fields with sentinel values handled gracefully
* 💎 fix: Stop Double-Counting Cache Tokens for Gemini/OpenAI in Usage Spend (#12855)
Different providers report `usage_metadata.input_tokens` with different
semantics:
- Anthropic / Bedrock: `input_tokens` EXCLUDES cache; cache reads/writes
arrive separately and must be added to get the total prompt size.
- Gemini / OpenAI: `input_tokens` ALREADY INCLUDES cached tokens
(Google's `promptTokenCount`, OpenAI's `prompt_tokens`). Their
`input_token_details.cache_*` are subsets of `input_tokens`.
`recordCollectedUsage` treated both schemes as additive, so for cache-hit
requests on Gemini/OpenAI it added cache tokens on top of an
`input_tokens` value that already contained them — overcharging users by
the cache_hit_rate (e.g., ~67% cache hit ≈ 1.67x overcharge). This
matches the issue reporter's GCP billing comparison.
Adds a small `splitUsage` helper that classifies the provider by model
name and computes `inputOnly` (the non-cached portion) plus the
all-inclusive `totalInput` for both the spend math and the returned
`input_tokens` summary. The helper defaults to additive semantics (the
historical behavior) so unknown providers are unaffected.
Updates existing OpenAI-shaped tests that previously asserted the buggy
additive math, and adds Gemini regression tests using the exact numbers
from the issue report (input=11125, cache_read=7441 → input=3684).
Anthropic / Bedrock paths remain bit-identical to before.
* 🔧 refactor: Classify Cache-Token Semantics by Provider, Not Model Name
Follows up the previous commit. Replaces a model-name regex
(`gemini|gpt|o[1-9]|chatgpt`) with an explicit `Providers` enum lookup
keyed off the `usage.provider` field — `UsageMetadata.provider` already
exists in `IJobStore.ts` but was never being populated.
- `callbacks.js#ModelEndHandler` now attaches `usage.provider` from
`agentContext.provider` alongside `usage.model`.
- `usage.ts` uses a `SUBSET_PROVIDERS` set (`openAI`, `azureOpenAI`,
`google`, `vertexai`, `xai`, `deepseek`, `openrouter`, `moonshot`)
backed by the canonical `Providers` enum from
`librechat-data-provider`.
- `xai`, `deepseek`, `openrouter`, `moonshot` extend `ChatOpenAI` so
they inherit subset semantics (verified in node_modules).
- Defaults to additive when `usage.provider` is missing, so the title
flow (which doesn't propagate provider) and any pre-this-PR usage
entries keep their existing behavior.
Tests: switch fixtures from model-name signaling to explicit `provider`
field, plus a Vertex AI case and a "missing provider" fallback case.
* 🛂 fix: Skip Re-Download of Inherited Code-Env Files (No More 403 Storms)
When a bash/code-interpreter call lists or operates on inputs the user
already owns (skill files primed via primeInvokedSkills, files inherited
from a prior session), codeapi echoes those files back in the tool
result with `inherited: true`. We were treating every entry as a
generated artifact and calling processCodeOutput on each, which:
1. Hit `/api/files/code/download/<session_id>/<file_id>` with the
user's session key. Skill files are uploaded under the skill's
entity_id, so every download 403'd — producing dozens of
"Unauthorized download" log lines per turn.
2. Surfaced those inputs as ghost file chips in the UI even though
they were never generated by the run.
3. Wasted a download round-trip even when no auth boundary was
crossed — the file is already persisted at its origin.
Fix: skip files where `file.inherited === true` in all three
artifact-files loops (`tools.js`, `createToolEndCallback`, and
`createResponsesToolEndCallback`). Skill files remain available to
subsequent calls via primeInvokedSkills / session inheritance — we
just don't redundantly re-download them.
Pairs with codeapi-side change that adds the `inherited` flag.
* 🔒 feat: Mark Skill Files as `read_only` During Code-Env Priming
Pairs with codeapi `read_only` upload flag (ClickHouse/ai#1345). When
LibreChat primes a skill into the code-env, every file in the batch
(SKILL.md plus all bundled scripts/schemas/docs) is now uploaded with
`read_only: true`. Codeapi seals these inputs at the filesystem layer
(chmod 444) and the walker echoes the original refs as `inherited:
true` regardless of whether sandboxed code modified the bytes on disk.
Without this, the previous PR's `inherited` skip handled only the
unchanged case. A modified skill file (pip writing pyc near a .py, a
script accidentally truncating LICENSE.txt, etc.) still flowed through
the modified-input branch on codeapi, got a fresh user-owned file_id,
uploaded as a "generated" artifact, and surfaced in the UI as a chip
the user couldn't actually authorize a download for.
Changes:
- `api/server/services/Files/Code/crud.js`:
`batchUploadCodeEnvFiles({ ..., read_only })` forwards the flag as
a multipart form field. Default `false` preserves existing behavior
for user-attached files and prior-session inheritance.
- `packages/api/src/agents/skillFiles.ts`: type signature gains
`read_only?: boolean`; `primeSkillFiles` passes `true`.
- `packages/api/src/agents/skillFiles.spec.ts`: assert the upload call
carries `read_only: true`.
The flag is intentionally not skill-specific. Any future
infrastructure-input flow (system fixtures, cached datasets, etc.) can
opt in the same way.
* 🪟 feat: Render Source-Code Artifacts in the Side Panel (CODE bucket)
PR #12832 wired markdown / mermaid / html / .jsx-tsx tool outputs through
the side-panel artifact pipeline but explicitly punted on code files:
> Everything else (csv, py, json, xls/docx/pptx, …) keeps PR #12829's
> inline behaviour — dedicated viewers will land in follow-ups.
This adds the code-file viewer. A `simple_graph.py` (and every other
common source file) now opens in the side panel alongside markdown,
mermaid, html, and react artifacts instead of falling back to the
inline `<pre>` rendering.
**Design.** New `CODE: 'application/vnd.code'` bucket reuses the static-
markdown sandpack template — `useArtifactProps` pre-wraps the source as
a fenced code block (` ```python\n...\n``` `) before handing it to
`getMarkdownFiles`. The fence carries a `language-<x>` class through
`marked`, so a future highlighter swap-in (e.g. drop `highlight.js`
into the markdown template) picks up syntax colors automatically. The
`react-ts` (sandpack) template's React boot cost is avoided since
source files don't need it.
**Single source of truth for languages.** New `CODE_EXTENSION_TO_LANGUAGE`
map drives BOTH:
- `EXTENSION_TO_TOOL_ARTIFACT_TYPE` routing (presence in this map =
code file). Adding a new language is one entry.
- The fenced-block language hint (exported as `languageForFilename`).
Identifiers follow the GitHub / `highlight.js` convention so the future
highlighter pickup is automatic.
**Scope.** Programming languages + stylesheets + shell + sql/graphql +
build files (Dockerfile/Makefile/HCL). Pure data formats
(CSV/TSV/JSON/JSONL/NDJSON/XML/YAML/TOML) and config dotfiles
(`.env`/`.ini`/`.conf`/`.cfg`) are intentionally NOT routed in this
pass — they're better served by dedicated viewers (CSV table view,
etc.) or remain inline. Adding them later is a one-entry change in
the map.
**JSX/TSX kept on the React (sandpack) bucket.** They're React component
sources; the existing live-preview should win over the static CODE
bucket. Plain `.js`/`.ts` source goes through CODE.
**MIME-type fallback.** The codeapi backend serves `text/x-python`,
`text/x-typescript`, etc. as `Content-Type` for source files, so a
file whose extension was stripped/renamed upstream still routes to
CODE via the MIME map.
**Empty-text gate.** CODE joins MARKDOWN/PLAIN_TEXT in the empty-text
exception (an empty `.py` is still a Python file). HTML/REACT/MERMAID
still require text — their viewers (sandpack/mermaid.js) error on
empty input.
**Files changed:**
- `client/src/utils/artifacts.ts` — `CODE` bucket constant,
`CODE_EXTENSION_TO_LANGUAGE` map, exported `isCodeExtension` and
`languageForFilename` helpers, extension/MIME routing additions,
template + dependencies entries, empty-text gate exception, helper
hoisting (extensionOf / baseMime moved up so the language map can
reference them).
- `client/src/hooks/Artifacts/useArtifactProps.ts` — exported
`wrapAsFencedCodeBlock`, CODE branch that wraps the source then
routes through `getMarkdownFiles`.
**Tests (+22):**
- 8 parameterized routing cases (.py, .js, .go, .rs, .css, .sh, .sql,
.kt) verify the CODE bucket fires.
- Extension wins when MIME is generic octet-stream (Python has no
magic bytes; common case).
- Regression: jsx/tsx STAY on REACT bucket (no live-preview regression).
- Regression: data formats (CSV/JSON/YAML/TOML) and config dotfiles
(.env/.ini) do NOT route to CODE.
- Empty-text exception for CODE (empty Python file is still a Python file).
- `useArtifactProps`: CODE → content.md / static template, fenced-block
shape, language hint, unknown-extension fallback to raw extension,
no-extension empty hint, index.html via markdown template.
- `wrapAsFencedCodeBlock`: language hint, empty hint, single-trailing-
newline trim, multi-newline preservation, empty-source emit.
87/87 in artifact-impacted tests; 155/155 across the broader artifact
suite. No regressions in pre-existing markdown/mermaid/HTML/REACT/text
behavior.
* 🛡️ fix: Bare-filename routing + adaptive fence delimiter (codex P2 ×2)
Two follow-ups from Codex review on the CODE bucket:
1. **Bare-filename routing for extensionless build files (Codex P2).**
`Dockerfile`, `Makefile`, `Gemfile`, `Rakefile`, `Vagrantfile`,
`Brewfile` have no `.` in their basename — `extensionOf` returns
`''` and the extension map can't match, so they fell through to
inline rendering despite being in `CODE_EXTENSION_TO_LANGUAGE`.
New `bareNameOf` helper returns the lowercased basename for
extensionless filenames (returns `''` for files with a `.` so the
extension and bare-name paths don't double-match). Both
`detectArtifactTypeFromFile` and `languageForFilename` consult it as
a second lookup against the same `CODE_EXTENSION_TO_LANGUAGE` map,
so adding a new build file is one entry. Path-aware: takes the
basename so `proj/Dockerfile` (path-preserving sanitizer output)
still routes correctly.
Added the four extra Ruby build-script names while I was here.
2. **Adaptive fence delimiter (Codex P2).** A hardcoded ` ``` ` fence
breaks when the source contains a line starting with ` ``` ` —
for example, a JS file containing a markdown-shaped template
literal:
const md = `
```
hello
```
`;
CommonMark closes a fence on a line whose backtick run matches-or-
exceeds the opener, so `marked` would close the outer fence at
the inner `\`\`\`` and the rest of the file would render as
markdown — corrupting the artifact and potentially altering
formatting / links outside `<code>`.
New `longestLeadingBacktickRun(source)` scans for the longest
start-of-line backtick run in the payload. Fence length =
`max(3, longest + 1)` — strictly more than any internal run, so
`marked` can never close the outer fence early. Only escalates
when needed; the common case still uses a triple-backtick fence.
Inline backticks (mid-line) don't count — they're not fence
delimiters. Only column-zero runs trigger escalation, so e.g.
a Python file with ` `inline ``` here` ` keeps the 3-fence.
+11 regression tests:
- 8 parameterized cases: `Dockerfile`/`Makefile`/`Gemfile`/etc. route
to CODE via bare-name fallback (case-insensitive on basename).
- Path-aware: `proj/Dockerfile` recognized.
- No double-match: `dockerfile.dev` (with extension) returns null.
- Unknown extensionless files (`README`, `LICENSE`) stay null.
- 4-backtick fence when source has ` ``` ` at start-of-line.
- 5-backtick fence when source has ` ```` ` at start-of-line.
- 3-backtick fence (default) for ordinary code.
- Inline backticks don't escalate.
- Source starting with backtick run at offset 0.
Plus 6 new `languageForFilename` tests covering bare-name fallback
and path-awareness.
108/108 in artifact-impacted tests (was 87, +21 tests). No regressions.
* 🛡️ fix: Indented fence detection + basename-scoped extensionOf (codex P2/P3)
Two follow-ups from the latest Codex review on the CODE bucket:
1. **Indented backtick runs (Codex P2).** `longestLeadingBacktickRun`
was scanning `^(`+)` — column 0 only. CommonMark allows fence
closers to be indented up to 3 spaces, so a JS source containing
an indented `\`\`\`` (e.g. inside a template literal embedded in a
class method) would still terminate our outer fence and the
remainder would render as markdown.
Updated regex to `^ {0,3}(`+)`. Tabs are not allowed in fence
indentation (CommonMark expands them to 4 spaces, which is over
the 3-space limit), so spaces alone suffice. Backticks indented
4+ spaces are CommonMark "indented code blocks" — they can't
terminate a fence, so we correctly don't escalate for them.
2. **`extensionOf` path-laden output (Codex P3).** `extensionOf` took
`lastIndexOf('.')` across the FULL path string, so
`pkg.v1/Dockerfile` yielded the nonsensical "extension"
`v1/dockerfile`. `languageForFilename` returned that as the language
hint (broken `language-v1/dockerfile` class on the fenced block),
AND the routing's bare-name fallback couldn't fire because the
extension lookup returned non-empty.
New `basenameOf` helper strips path separators; `extensionOf` and
`bareNameOf` both go through it. After the fix:
- `pkg.v1/Dockerfile` → `extensionOf` returns `''` → `bareNameOf`
returns `dockerfile` → routes to CODE with correct language.
- `pkg.v1/main.go` → `extensionOf` returns `go` → routes correctly.
- `pkg.v1/script.py` → `extensionOf` returns `py` → routes correctly.
+10 regression tests:
- 5 parameterized cases covering 1-3 space indent at fence lengths
3, 4, 5 (escalation kicks in correctly).
- 4-space indent does NOT escalate (CommonMark indented-code-block
territory; can't close a fence).
- `pkg.v1/Dockerfile` and `a.b.c/Makefile` route to CODE +
`languageForFilename` returns `dockerfile`/`makefile`.
- Dotted-directory files (`pkg.v1/main.go`, `a.b.c/script.py`) still
route correctly via the basename-scoped extension parse.
118/118 in artifact-impacted tests (was 108, +10 tests). No regressions.
* 🛡️ fix: Comprehensive review polish + MIME-derived language hint (codex P3)
Resolves all 8 valid findings from the comprehensive review and the
follow-up Codex P3 on the same PR. None are user-visible bugs; the set
spans correctness guards, dead-code removal, organization, and test
coverage.
**Comprehensive review #1 — Remove dead `isCodeExtension` export.**
Function was exported with zero callers anywhere in the codebase.
**Comprehensive review #2 — Guard the for-loop against silent overwrites.**
The `for (ext of CODE_EXTENSION_TO_LANGUAGE)` loop blindly assigned
each language extension to the CODE bucket. If a future contributor
added `jsx` or `tsx` to the language map (a natural mistake — they
ARE source code), the loop would silently overwrite the REACT bucket
entries and break the sandpack live-preview with no compile-time or
runtime error. Added `if (ext in EXTENSION_TO_TOOL_ARTIFACT_TYPE) continue`
so explicit map entries always win.
**Comprehensive review #3 — Add `fileToArtifact` end-to-end test for CODE.**
Routing was tested via `detectArtifactTypeFromFile`; full Artifact
construction (id / type / title / content / messageId / language) for
CODE was not. Added 5 new `fileToArtifact` cases.
**Comprehensive review #4 — Move pure utilities out of the hook file.**
`wrapAsFencedCodeBlock` and `longestLeadingBacktickRun` are pure
string transformations with no React dependencies. Moved both to
`utils/artifacts.ts`. Test files updated to import from the new
location.
**Comprehensive review #5 — Correct the MIME-map "mirrors" comment.**
Comment claimed the MIME map mirrored `CODE_EXTENSION_TO_LANGUAGE`, but
covered ~21 of ~60 entries. Reworded to "best-effort COMMON-CASE list,
not an exhaustive mirror" with the rationale (extension routing is
primary; MIME is a stripped-filename fallback).
**Comprehensive review #6 — Drop `lang ? lang : ''` ternary.**
`lang` is typed `string`; the only falsy value is `''`. Removed.
(Replaced via the MIME-fallback rewrite of `wrapAsFencedCodeBlock`,
where `lang` is now used directly without the ternary.)
**Comprehensive review #7 — Avoid double `basenameOf` computation.**
`extensionOf(filename)` and `bareNameOf(filename)` both internally
called `basenameOf` — when the extension lookup missed,
`detectArtifactTypeFromFile` paid for two parses of the same path.
Split into private `extensionFromBasename` / `bareNameFromBasename`
helpers; the caller computes `basenameOf` once and threads it through.
**Comprehensive review #8 — Trim verbose Dockerfile/Makefile comment.**
Inline comment block in the language map duplicated `bareNameOf`'s
JSDoc. Replaced with a one-line pointer.
**Codex P3 — MIME fallback for the CODE language hint.**
`detectArtifactTypeFromFile` routes `{ filename: 'noext', type:
'text/x-python' }` to CODE via the MIME bucket map, but then
`useArtifactProps` derived the language hint from `artifact.title`
ONLY — and `noext` has no extension, so `languageForFilename` returned
empty and the fenced block emitted with no `language-` class. The
future highlighter swap-in would lose syntax-color metadata for these
files.
- New `MIME_TO_LANGUAGE` map covering the language MIMEs codeapi
actually emits.
- `languageForFilename(filename, mime?)` now takes an optional MIME
second arg and falls back to it after the extension and bare-name
paths.
- `fileToArtifact` resolves the language at construction time
(using both filename AND `attachment.type`) and stores it on
`artifact.language`. The hook reads `artifact.language` directly
rather than re-deriving from `title` alone, so the MIME signal
survives end-to-end.
- Title-derived fallback in the hook covers older callers that
don't populate `language`.
Tests:
+10 cases for the comprehensive review findings (CODE end-to-end
via `fileToArtifact`, language storage, non-CODE language
un-set).
+6 cases for the MIME fallback (`languageForFilename(name, mime)`
ordering, MIME parameter stripping, extension/bare-name vs MIME
precedence, empty signal).
+2 hook tests for `artifact.language` pre-resolved vs title-fallback.
131/131 in directly-impacted files (was 118, +13).
199/199 across the broader artifact suite. No regressions.
Pre-existing TypeScript errors in `a11y/`, `Agents/`, `Auth/`,
`Mermaid.tsx`, etc. are unrelated to this PR (verified by checking
`tsc --noEmit` on origin/dev — same errors).
* 📂 fix: Preserve Nested Folder Paths for Code-Execution Artifacts
When codeapi reports a generated file at a nested path (`a/b/file.txt`),
`processCodeOutput` was running it through `sanitizeFilename` — which
calls `path.basename()` and then collapses `/` to `_`. The DB row ended
up with `filename: "file.txt"`, `primeFiles` shipped that flat name back
to the next sandbox session, and `cat /mnt/data/a/b/file.txt` 404'd.
Fix: split the sanitizer into two helpers in `packages/api/src/utils/files.ts`:
- `sanitizeArtifactPath` — segment-wise sanitize while preserving
`/`. Falls back to basename on `..` traversal, absolute paths, and
other malformed inputs. The DB record uses this so the next prime()
can recreate the nested path in the sandbox.
- `flattenArtifactPath` — encode `/` as `__` for the local
`saveBuffer` strategies, which key by single-component filename and
would otherwise create unintended subdirectories under uploads/.
`process.js` is updated to use both: DB filename keeps the path, storage
key flattens. `claimCodeFile` is also keyed on `safeName` so the
(filename, conversationId) compound key stays consistent with the
record `createFile` writes.
Tests:
+13 unit tests in `files.spec.ts` (sanitizeArtifactPath table,
flattenArtifactPath round-trip).
+1 integration test in `process.spec.js` asserting the DB-row vs
storage-key split for a nested path.
Updated `process-traversal.spec.js` to mock the new helpers.
64 pass / 0 fail across `Files/Code/`; 36 pass / 0 fail in
`packages/api/src/utils/files.spec.ts`.
Companion: ClickHouse/ai#1327 — the codeapi-side counterpart that stops
phantom file IDs from reaching this code path in the first place. These
two are independent but the matplotlib bug is most cleanly resolved when
both ship.
* 🛡️ fix: Re-add 255-char per-segment cap in sanitizeArtifactPath (codex review P2)
`sanitizeArtifactPath` dropped the 255-char basename cap that
`sanitizeFilename` enforces. Long artifact names then flowed unbounded
into `processCodeOutput`'s storage key (`${file_id}__${flatName}`) and
tripped `ENAMETOOLONG` on filesystems that enforce `NAME_MAX` —
saveBuffer fails, and the file falls back to a download URL instead of
persisting / priming. This was a regression specifically for flat
filenames that the original `sanitizeFilename` would have truncated
safely.
Re-add the cap as a per-path-component limit so it applies cleanly to
both flat and nested paths:
- Leaf segment: extension-preserving truncation, matching
`sanitizeFilename`'s shape (`<truncated-stem>-<6 hex>.<ext>`).
- Non-leaf (directory) segments: plain truncate-and-disambiguate
(`<truncated-name>-<6 hex>`); directory names don't carry semantic
extensions worth preserving.
- Defensive fallback when `path.extname` returns a pathologically long
"extension" (e.g. `_.aaaa…aaa` after the dotfile underscore prefix
rewrite turns a long hidden file into a non-dotfile with a 300-char
"extension"): collapse to whole-segment truncation rather than
leaving the cap unmet.
+6 unit tests covering: long leaf (regression case), long leaf under a
preserved directory, long non-leaf segment, deeply nested mixed-length,
exact-255 boundary (no truncation), and the dotfile + truncation
interaction.
* 🛡️ fix: Cap flattened storage key against NAME_MAX in processCodeOutput (codex review P1)
Per-segment caps on the path-preserving form aren't enough. Once segments
are joined with `__` for the storage key, deeply-nested or moderately
long paths can still produce a flat form that overflows once
`${file_id}__` is prepended — `${file_id}__a__b__c.csv` for a 3-level
100-char-each path is ~344 chars, well past filesystem NAME_MAX (255).
saveBuffer then trips ENAMETOOLONG and falls back to a download URL,
and the artifact never persists / primes.
`flattenArtifactPath` gets an optional `maxLength` parameter. When set,
the function truncates the flat form to fit, preserving the leaf
extension with the same disambiguating-hex-suffix shape sanitizeFilename
uses. Default (`undefined`) keeps existing call sites uncapped — the cap
is opt-in for callers that are actually building a filesystem key.
Pathologically long "extensions" from `path.extname` (e.g. `.aaaa…aaa`)
fall back to whole-key truncation rather than leaving the cap unmet.
processCodeOutput composes the storage key after `file_id` is known and
passes `255 - file_id.length - 2` as the budget so the full
`${file_id}__${flatName}` string fits in one filesystem path component.
+7 unit tests in files.spec.ts:
- Pass-through when no maxLength supplied (cap is opt-in).
- Pass-through when flat form fits within maxLength.
- Truncation with leaf extension preserved (the regression case).
- Leaf-only overflow with extension preservation.
- Pathological long-extension fallback (whole-key truncation).
- No-extension stem truncation.
- Boundary equality (off-by-one guard).
+1 integration test in process.spec.js: processCodeOutput passes the
file_id-aware budget (`255 - file_id.length - 2`) to flattenArtifactPath.
114/114 across files.spec.ts + Files/Code (49 + 65).
* 🛡️ fix: Determinize + clamp artifact-path truncation (codex review P2 ×2)
Two follow-ups to Codex review on the path/flat-key cap:
1. **Deterministic truncation suffixes**. The previous helpers used
`crypto.randomBytes(3)` for the disambiguator, mirroring
`sanitizeFilename`'s shape. That made the truncated form non-
deterministic: a re-upload of the same long filename would compute a
*different* storage key, orphaning the previous on-disk file under
the reused `file_id` returned by `claimCodeFile`.
New `deterministicHexSuffix(input)` helper hashes the input with
SHA-256 and takes the first 6 hex chars. Same input → same suffix
(storage key stable across re-uploads); different inputs sharing a
truncation prefix still get different suffixes (collision avoidance).
24 bits ≈ 16M values is collision-safe for our scale (single-digit
artifacts per turn per (filename, conversationId) bucket).
Applied to `truncateLeafSegment`, `truncateDirSegment`, and
`flattenArtifactPath` — every truncation site in the new helpers.
`sanitizeFilename` (pre-existing) is intentionally left alone; its
tests rely on the random-bytes mock and it's outside this PR's scope.
2. **Final clamp on flattenArtifactPath result**. The old `Math.max(1,
maxLength - ext.length - 7)` floor could let the result slip past
`maxLength` when the extension was nearly as large as the budget
(e.g. `maxLength=5`, `ext=".txt"`: budget computed as 0, but result
was `-<6 hex>.txt` = 11 chars). Drop the `Math.max(1, …)` floor and
add a final `truncated.slice(0, maxLength)` so the contract holds
for any input. Also short-circuit `maxLength <= 0` to `''` for
pathological budgets.
Tests updated to compute the expected hash inline (the existing
`randomBytes` mock doesn't apply to the new code path), plus 4 new
regression tests:
- sanitizeArtifactPath: same input → same output, different inputs →
different outputs (determinism + collision avoidance).
- flattenArtifactPath: same input → same output, different inputs
sharing a truncation prefix → different outputs.
- flattenArtifactPath: clamp holds when ext.length > maxLength - 7.
- flattenArtifactPath: returns '' for maxLength <= 0.
53 unit tests pass. 65 integration tests pass.
* 🛡️ fix: Total-path cap + basename for classifier (codex P2 + comprehensive review)
Four follow-ups from the latest reviews on this PR:
1. **Codex P2: total-path cap in sanitizeArtifactPath**. Per-segment
caps weren't enough — a deeply nested path (3+ at-cap segments) can
still produce a joined form past Mongo's 1024-byte indexed-key limit
(4.0 and earlier reject; later versions configurable). Added
`ARTIFACT_PATH_TOTAL_MAX = 512` and a leaf-only fallback when the
joined form exceeds it. Same shape as the absolute-path /
`..`-traversal fallbacks above; the leaf is already segment-capped to
≤255, so the final result stays within bounds.
2. **Codex P2: pass basename to classifier/extractor in process.js**.
With the path-preserving sanitizer, `safeName` can now be a nested
string like `reports.v1/Makefile`. The classifier's `extensionOf`
reads that as `v1/Makefile` (the slice after the dot in the directory
name) and the bare-name branch rejects because it sees a `.`
anywhere. Result: extensionless artifacts under dotted folders
(Makefile, Dockerfile, etc.) get misclassified as `other` and skip
text extraction. Pass `path.basename(safeName)` to both
`classifyCodeArtifact` and `extractCodeArtifactText` so
classification matches what the old flat-name flow produced.
3. **Review nit: drop dead `sanitizeFilename` mock in process.spec.js**.
process.js no longer imports `sanitizeFilename`; the mock was
misleading dead code.
4. **Review nit: rename misleading `'embedded parent traversal'` test**.
`path.posix.normalize('a/../escape.txt')` resolves to `escape.txt`
which goes through the normal segment-split path, not the
`sanitizeFilename` fallback. Test name now says "resolves embedded
parent traversal via path normalization" to match the actual code
path.
+3 regression tests:
- sanitizeArtifactPath falls back to leaf-only when joined > 512.
- sanitizeArtifactPath keeps nested path within the 512 budget.
- process.spec: passes basename (`Makefile` from `reports.v1/Makefile`)
to classifyCodeArtifact + extractCodeArtifactText.
Existing "caps every segment in a deeply-nested path" test now uses 2
segments (not 3) so the joined form stays under the new total cap; the
3-segment scenario is covered by the new fallback test instead.
55 unit + 66 integration = 121/121 pass.
* 📝 docs: Correct sanitizeArtifactPath JSDoc to match actual schema index
Two doc-only fixes from the latest comprehensive review (both NIT):
1. **Index field list was wrong**. JSDoc claimed the compound unique
index was `{ file_id, filename, conversationId, context }`. The
actual index in `packages/data-schemas/src/schema/file.ts:92-95` is
`{ filename, conversationId, context, tenantId }` with a partial
filter for `context: FileContext.execute_code`. The cap rationale
(Mongo 4.0 indexed-key limit) is correct and unchanged; just the
field list was wrong. Added the schema file path so future readers
can find the source of truth.
2. **Trade-off acknowledgement**. The reviewer noted that the
leaf-only fallback loses directory structure, which means the
model's `cat /mnt/data/<deep>/<path>/file.txt` would 404 on the
pathological-depth case — partially re-introducing the original
flat-name bug for >512-char paths. This is intentional (DB write
failure is strictly worse than losing structure), but the trade-off
wasn't called out explicitly in the JSDoc. Added a paragraph
acknowledging it and noting that the cap is monotonically better
than the pre-PR behavior, where ALL artifacts were treated this way
regardless of depth.
No code or test changes — pure JSDoc correction. Tests still 55/0.
* 🛡️ fix: Disambiguate sanitized artifact names to keep claimCodeFile keys unique (codex P2)
`sanitizeArtifactPath` is not injective — multiple raw inputs can collapse
onto the same regex-and-normalize output. Codex's example:
`reports 2026/out.csv` and `reports_2026/out.csv` both sanitize to
`reports_2026/out.csv`. `claimCodeFile` is keyed on the schema's compound
unique `(filename, conversationId, context, tenantId)` index, so the
later upload silently matches the earlier record and overwrites the first
artifact's bytes via the reused `file_id` — a single conversation can
drop files when both names are valid in the sandbox.
This collision space isn't strictly new — pre-PR `sanitizeFilename`
(basename-only) had the same property — but the path-preserving form
gives us enough information to fix it for the first time.
**Fix.** When character-level sanitization changed something (regex
replacement, path normalization, dotfile prefix, empty-segment collapse),
embed a deterministic SHA-256 prefix of the **raw input** in the leaf
segment via the new `embedDisambiguatorInLeaf` helper. Same raw input →
same safe form (idempotent for re-uploads); different raw inputs that
would have collided → different safe forms.
**Why "character-level"** specifically:
- The disambiguator fires when `preCapJoined !== inputName` (post-regex
+ dotfile + empty-segment, BUT pre-truncation).
- Truncation alone is already disambiguated by `truncateLeafSegment`'s
own seg-hash; firing the input-hash branch on truncation would just
stack a second hash for no collision-avoidance benefit and clutter
human-readable filenames.
**Three known collision shapes covered:**
1. `out 1.csv` vs `out_1.csv` (and `out@1.csv` vs `out#1.csv`, etc.)
2. `dir//file.txt` vs `dir/file.txt` (empty-segment collapse)
3. `.x` vs `_.x` (dotfile-prefix step)
**Disambiguator + truncation interaction:** for very long mutated leaves,
`truncateLeafSegment` caps at 255 first, then `embedDisambiguatorInLeaf`
re-trims to insert the input hash. The seg-hash from the first pass is
replaced by the input-hash from the second pass — that's intentional
(input-hash is the load-bearing collision-avoidance suffix; seg-hash was
only ever decorative once the input-hash exists). Final clamp ensures
the result never exceeds `ARTIFACT_PATH_SEGMENT_MAX` regardless of input.
**Disambiguator + total-cap fallback:** when joined > 512, we fall back
to the leaf-only form. The leaf has already had the disambiguator
embedded, so collision avoidance survives the pathological-depth case.
**`embedDisambiguatorInLeaf`** uses `dot <= 1` to detect "no real
extension" (covers extensionless names AND dotfile-prefixed leaves like
`_.hidden` — without this, `_.hidden` would split as stem `_` + ext
`.hidden` and produce the awkward `_-<hash>.hidden`).
**Updated 5 existing tests** that asserted the old collision-prone
outputs — they now verify the disambiguator-included form. The
character-level-only firing rule was load-bearing here: tests for
"clean inputs (no mutation)" and "long inputs (truncation only)" still
pass without any disambiguator clutter.
**+7 regression tests** in a new `collision avoidance (Codex review P2)`
describe block:
1. Different raw inputs sanitizing to the same form get distinct safes
2. Whitespace-vs-underscore in directory segment
3. Dotfile-prefix collision
4. Idempotency: same raw → same safe across calls
5. Clean inputs skip the disambiguator (cosmetic guarantee)
6. Disambiguator survives leaf truncation (long mutated leaf)
7. Disambiguator survives total-cap fallback (pathological depth)
62 unit + 66 integration = 128/128 pass.
Two test-file hygiene fixes:
1. **Literal NUL bytes**. The `'rejects binary content (NUL bytes)
post-fetch'` test embedded raw `\x00` bytes directly in the source
string (`const binaryWithNul = '<3 NULs>\rIHDR<2 NULs>\x04'`).
Embedding NULs in source files breaks editors, linters, ts-loader,
and most git tooling — `grep` even classifies the file as binary
("Binary file matches"). Replace with `\x00` escape sequences in the
string literal so the source is plain ASCII while the runtime string
value is unchanged.
2. **CRLF line endings**. My earlier commits to this file picked up
Windows-style `\r\n` from git's `core.autocrlf=true` checkout
conversion, then staged them as `\r\n`. The diff against `dev`
showed the entire file as changed even though only a few lines were
touched semantically. Normalize the whole file back to LF so future
diffs read clean.
The diff for this commit is large (~1248 lines marked changed) but
every change is one of: CRLF → LF, or the single `binaryWithNul`
escape-sequence rewrite. No semantic test changes.
Tests: 39/39 pass (unchanged behavior).
* 🚫 fix: Reject Binary Files in read_file Sandbox Fallback (No More Mojibake)
`read_file("/mnt/data/simple_graph.png")` was shelling `cat` through codeapi
`/exec` and shipping the result back to the model. codeapi's transport is
JSON, so `stdout` containing PNG bytes round-tripped through lossy UTF-8
replacement (every non-ASCII byte became U+FFFD), got line-numbered by
`addLineNumbers`, and arrived in the model's context as a multi-KB blob of
`�PNG\r\n 2 | ...`. The bytes were unrecoverable — and the same
codeapi sandbox logged the base64-style mojibake too — so the goal is
fail-fast, not retrieval.
Two guards in `handleSandboxFileFallback`, both bypassed by the existing
text path:
1. **Extension precheck** (BEFORE any network call) for known-binary
types: images, documents (pdf/docx/xlsx/etc), archives, audio, video,
native libs, fonts, and a few other byte-soup formats. The message
for image extensions points at the existing chat attachment ("the
image is already shown to the user, use bash_tool for programmatic
processing"); other binaries get the generic "use bash" hint.
2. **NUL-byte sniff** (AFTER the read) on the first 8KB for unknown
extensions or no-extension paths. The codeapi `/exec` JSON encoding
mangles most non-UTF-8 bytes but a NUL terminator from a magic
header survives the round-trip, so this catches novel binary
formats without an extension precheck.
`lowercaseExtension` uses the basename to avoid false-triggering on
directory-name dots (e.g. `proj.v1/notes` has no extension, not `.v1/notes`).
+6 tests:
- Image rejected by extension, never calls readSandboxFile, message
points at the existing attachment.
- Non-image binary (.zip) rejected with a different (bash-only) message.
- Case-insensitive extension match (.PNG vs .png).
- NUL-byte sniff catches unknown-extension binary post-fetch.
- Text files with binary-adjacent extensions (.txt) still readable.
- Dotted directory names don't false-trigger the extension match.
38/38 handlers.spec.ts pass.
The companion bash "command not found" issue from the same conversation
is a separate LLM mistake (writing raw Python as the bash command without
`python3 -c` / heredoc wrapper). Not coded here — flagged to the user.
* 🖼️ fix: Allow SVG read_file (XML text, no mojibake risk) — codex review P2
`.svg` was bucketed with raster-image extensions in
`BINARY_EXTENSIONS_NEVER_READABLE`, which made `handleSandboxFileFallback`
reject every SVG before calling readSandboxFile. SVG is an XML text
format — there's no mojibake risk for normal content, and the model has
legitimate reasons to inspect or edit a generated SVG (tweaking colors,
paths, viewBox, etc.). Block was a regression for valid read_file usage.
Remove `.svg` from both `BINARY_EXTENSIONS_NEVER_READABLE` (so it routes
through the normal sandbox read path) and `IMAGE_EXTENSIONS_FOR_HINT`
(now-dead entry — only used by the rejection-message picker). The
post-fetch NUL-byte sniff still catches anything that turns out to be
binary despite a `.svg` extension.
+1 regression test that an SVG with valid XML content reads through
successfully (`<svg>...<circle/>...</svg>` → status: 'success', content
contains `<svg`/`viewBox`).
* 🪟 feat: Render Code-Execution Text Artifacts as Side-Panel Artifacts
Builds on PR #12829 (which populates `text` on code-execution file
attachments). When a tool-output file's extension/MIME maps to a
viewer we already have, route it through the artifact UI instead of
the inline `<pre>`:
- text/html, text/htm → existing artifacts side panel (sandpack)
- App.jsx / App.tsx → existing artifacts side panel (sandpack)
- *.md / *.markdown / *.mdx → existing artifacts side panel (sandpack)
- *.mmd / *.mermaid → standalone Mermaid component, inline
(no sandpack/react template)
The card and the mermaid header both expose a download button so the
underlying file is still reachable. Everything else (csv, py, json,
xls/docx/pptx, …) keeps PR #12829's inline behaviour — dedicated
viewers for csv/docx/xlsx/pptx will land in follow-ups.
Backend: `.mmd` and `.mermaid` added to UTF8_TEXT_EXTENSIONS so
mermaid sources reach the client with `text` populated.
Frontend changes:
- `client/src/utils/artifacts.ts` — `TOOL_ARTIFACT_TYPES` constant,
`detectArtifactTypeFromFile`, `fileToArtifact` (id is derived from
`file_id` so the same artifact across renders dedupes cleanly).
- `client/src/components/Chat/Messages/Content/Parts/ToolArtifactCard.tsx`
— registers the artifact in `artifactsState`, renders an
`ArtifactButton`-style trigger paired with a download button.
- `client/src/components/Chat/Messages/Content/Parts/ToolMermaidArtifact.tsx`
— wraps the standalone Mermaid component with a filename + download
header so the file stays reachable.
- `Attachment.tsx` and `LogContent.tsx` — gain panel-artifact and
mermaid branches in the routing decision tree, ahead of the existing
inline-text fallback. Existing branches untouched.
Test coverage: backend extension matrix (mmd/mermaid), frontend
predicates (`isPanelArtifact`, `isMermaidArtifact`,
`artifactTypeForAttachment`), `fileToArtifact`, and an RTL suite that
verifies each type routes to the right component (panel card / mermaid
render / inline pre / file chip).
* 🩹 fix: Address review on code-artifacts-panel routing
- ToolArtifactCard: defer artifact registration to the click handler so
rendering a card never side-effects into `artifactsState`. With
`artifactsVisibility` defaulting to `true`, eager mount-time
registration would surface tool artifacts in the side panel without
user intent — now matches ArtifactButton's pattern. Drop the
redundant `artifacts` subscription (write-only via useSetRecoilState).
- LogContent.tsx: precompute `Artifact`s inside the existing useMemo
bucket-sort so each render isn't producing fresh objects. Without
this, missing updatedAt/createdAt fields would make `toLastUpdate`
return `Date.now()` and churn Recoil state on every parent render.
- Attachment.tsx + LogContent.tsx: classify each attachment once via
`artifactTypeForAttachment` and branch on the result, instead of
calling `isMermaidArtifact` and `isPanelArtifact` back-to-back
(each of which internally re-classified). AGENTS.md single-pass rule.
- artifacts.ts `detectArtifactTypeFromFile`: strip `;` parameters
before the MIME comparison (so `text/html; charset=utf-8` is
recognized) and add fallbacks for `application/vnd.react`,
`application/vnd.ant.react`, and `application/vnd.mermaid`.
- ToolMermaidArtifact: drop the `id` prop entirely when `file_id` is
undefined so we never pass an undefined DOM id through to mermaid.
- AttachmentGroup: keys derived from `file_id` (not bare index) so
add/remove churn doesn't remount stable cards.
- Wrappers (PanelArtifact / MermaidArtifact / ToolMermaidArtifact)
tightened from `Partial<TAttachment>` to `TAttachment` since the
caller always passes a full attachment.
- fileToArtifact: drop dead `?? ''` on content (guarded by the
preceding type check).
- Tests: new click-interaction suite verifying the deferred-registration
invariant, click registers + opens panel, and second click toggles
closed without losing the registered artifact.
* 🧹 chore: Address follow-up review NITs
- artifacts.test.ts: regression-pin baseMime() with charset/case
variants for text/html, text/markdown, application/vnd.react.
- attachmentTypes.ts: drop the now-unused isMermaidArtifact and
isPanelArtifact wrappers (the routing collapsed onto a single
artifactTypeForAttachment call in the previous commit, so they
were only kept alive by their own test). attachmentTypes.test.ts
rewritten to exercise artifactTypeForAttachment branches directly.
- Attachment.tsx + LogContent.tsx: re-sort the local imports
longest-to-shortest per AGENTS.md (~/utils/artifacts is 72 chars
and was sitting after a 51-char import).
* ✨ feat: Auto-open panel + route txt/docx/odt/pptx through artifacts
- artifacts.ts: add `text/plain` to TOOL_ARTIFACT_TYPES so plain-text
documents (and the markdown-like ones we don't have rich viewers for
yet) can route through the side panel. `useArtifactProps` already
dispatches `text/plain` to the markdown-style template, so they
render cleanly with no panel-side change.
- Extension map gains txt/docx/odt/pptx → text/plain. pptx is wired
up speculatively — backend extraction is still deferred, so the
routing fires the moment that lands. The MIME map gets the matching
office MIME types for symmetry (extension wins, but it's nice to
have the fallback when sniffing returns the canonical office MIME).
- ToolArtifactCard: register the artifact in `artifactsState` on
mount again. With visibility defaulting to `true` and the panel's
`useArtifacts` hook auto-selecting the latest artifact, this gives
the auto-open behaviour that the legacy streaming artifacts have.
Click handler is now just "focus + reveal" (registration already
happened); a user who has explicitly closed the panel keeps it
closed and uses the click to re-open.
- Tests: parameterised row for each new extension; ArtifactRouting
invariant flipped from "no register on mount" to "registers on
mount so panel can auto-open". Existing TextAttachment test that
used `a.txt` switched to `a.csv` since `.txt` now panel-routes.
* 🐛 fix: Auto-focus latest tool artifact + self-heal after panel close
Two bugs in the previous commit's auto-open behaviour:
1. After closing the side panel, no artifact card could be reopened.
`useArtifacts.ts` resets `artifactsState` in its unmount cleanup
(line 50), which fires when visibility goes to `false`. The card's
mount-only `useEffect` doesn't refire after that wipe, so the
subsequent click set `currentArtifactId` to an id that was no
longer in `artifactsState`, and `Presentation.tsx` then refused to
render the panel because `Object.keys(artifacts).length === 0`.
Fix: the registration `useEffect` now has no dependency array, so
it self-heals after the wipe (the dedup check keeps it cheap when
nothing actually needs writing).
2. Newly-arrived artifacts didn't steal focus from an already-selected
one. `useArtifacts`'s fallback auto-select (line 64) only fires
when `currentId` is null or no longer in the list — it deliberately
protects an existing selection, while the streaming-specific effect
that handles legacy focus-stealing is gated on `isSubmitting`.
That gate doesn't apply to tool-output artifacts.
Fix: a second `useEffect` keyed on `artifact.id` calls
`setCurrentArtifactId(artifact.id)` whenever a new card mounts.
Cards mount in attachment-array order, so the LAST-mounted card
(the newest tool output) wins — matching the legacy "latest auto-
opens" UX.
Tests: replace the now-stale "no register on mount" assertion with
"registers and auto-focuses on mount", flip the toggle test to start
from the auto-focused state, and add two regression tests covering
the close-then-reopen path and the latest-of-many auto-focus.
* ✨ feat: Route pptx through artifact panel with placeholder content
Before this commit, pptx files fell through to a plain FileContainer
chip even though the extension was wired into the artifact map: backend
text extraction is still deferred for pptx, so `attachment.text` came
back null/empty and `detectArtifactTypeFromFile`'s strict text check
returned null. That meant docx/odt rendered as proper artifact cards
while pptx in the same message rendered as a tiny download chip.
`detectArtifactTypeFromFile` now allows empty text for the plain-text
and markdown buckets, since their viewers (the markdown template) handle
empty content gracefully. HTML / React / Mermaid still require real
content because sandpack and mermaid.js error on empty input.
`fileToArtifact` substitutes a markdown placeholder
("Preview not available yet — click Download to view the file.") when
the file routes through the panel without text. The panel renders the
placeholder via the markdown template; pptx (and any docx that fails
extraction) gets visual parity with its siblings, and the moment
backend extraction lands the placeholder is replaced by real content
without any frontend change.
Tests: split the "no text returns null" assertion into the strict
viewers (HTML/React/Mermaid) and the lenient ones (plain-text/markdown);
add a fileToArtifact case proving pptx without text gets the
placeholder, and another proving real text wins when present.
* ✨ feat: Dedup duplicate tool-artifact cards across tool calls + messages
Two `ToolArtifactCard` instances for the same file_id (e.g. agent reads
back what it just wrote, or the same file is referenced in turns 1 and
5) now collapse to a single chip — the most recent mount wins, the
older sibling re-renders to `null`.
Implementation:
- New `toolArtifactClaim` atomFamily keyed by artifact id. Each card
generates a unique component-instance key via `useId()`, claims the
slot in a `useLayoutEffect` (synchronous before paint, no flicker),
and releases it on unmount only if the claim is still ours. A later
card with the same id overwrites the claim → earlier card subscribes
via `useRecoilState` and renders `null`.
- Family-keyed (per artifact id) so adding/removing a claim for one
file never re-renders cards for unrelated files. Addresses the
"messages view re-renders frequently" concern: each card subscribes
only to its own slice.
- `ToolMermaidArtifact` shares the same atom via the new exported
`toolArtifactKey()` helper, so the same `.mmd` file can't double-
render either.
- Latest content always wins for the panel because the eager
`setArtifacts` registration is last-write-wins on `artifactsState`
by id — independent of which card holds the claim. Updating a file
refreshes the panel content even if the chip's visual location
doesn't move.
Tests: two new cases asserting that duplicate panel and mermaid
attachments collapse to a single rendered card.
* 🧹 chore: Address comprehensive review on code-artifacts-panel
- ToolArtifactCard self-heal now subscribes to a per-id selector
(`artifactByIdSelector`) instead of a no-deps `useEffect`. Effect
deps are `(artifact, existingEntry, setArtifacts)` so it runs
deterministically when the slice transitions to undefined (panel-
unmount cleanup) or when artifact content drifts — not on every
parent render. Each card subscribes only to its own slice via the
selectorFamily, so unrelated state changes don't re-render.
- artifacts.ts: localize the empty-content placeholder via a new
`fileToArtifact(attachment, options?)` signature. Callers in
`Attachment.tsx` (PanelArtifact) and `LogContent.tsx` resolve
`com_ui_artifact_preview_pending` from `useLocalize` and thread
it in. Default is empty string when no placeholder is supplied.
- artifacts.ts: thread `preClassifiedType` through `fileToArtifact`
so the routing decision tree's `artifactTypeForAttachment` call
is the only classification — previously `fileToArtifact` re-ran
`detectArtifactTypeFromFile` after the routing already had the
answer. Bucket type updated to `Array<{ attachment, type }>`.
- artifacts.ts: drop bare `text/plain` from `MIME_TO_TOOL_ARTIFACT_TYPE`.
The extension map handles `.txt` explicitly; routing every
unrecognized-extension `text/plain` file (extensionless scripts,
`.env`, etc.) through the panel was a wider catch than the PR
scope intended.
- artifacts.ts: stable `toLastUpdate` fallback of `0` (was
`Date.now()`). `useArtifacts` sorts by `lastUpdateTime`, so a fresh
timestamp on every call would re-sort entries non-deterministically
across renders.
- artifacts.ts: drop dead `toolArtifactId = toolArtifactKey` alias.
Add `filepath` to the key-derivation fallback chain so two
unnamed-and-unidentified files don't collide on the literal
`tool-artifact-unknown` key.
- ToolArtifactCard import order: package types before local types.
- store/artifacts.ts: JSDoc on `toolArtifactClaim` documenting the
atomFamily-entries-persist-after-unmount trade-off (entries reset
to null on card unmount; total cost is one key + a null per
artifact — fine at typical session scale).
- Tests:
- Updated existing `fileToArtifact` placeholder assertion to use
the caller-provided string.
- New: panel routing skips re-classification when
`preClassifiedType` is provided.
- New: bare `text/plain` MIME with unrecognized extension does
NOT route through the panel.
- New `LogContent.test.tsx` (6 cases) — HTML→panel, mermaid→
inline, CSV→inline `<pre>`, archive→download chip, pptx→
placeholder card, mixed split.
- Dedup tests rewritten to use two AttachmentGroups (matching
the real per-tool-call render) instead of a same-array
duplicate that triggered React's duplicate-key warning.
* 🩹 fix: Address codex review + comprehensive review NITs
codex (P2):
- artifacts.ts: switch placeholder fallback to nullish coalescing.
Empty string is now preserved as legitimate content (a 0-byte `.md`
or `.txt` is a valid artifact, not "extraction unavailable") —
only `null`/`undefined` triggers the deferred-extraction placeholder.
- Attachment.tsx: derive React keys via a new `renderKey` helper that
combines `file_id` with the array index. Prevents duplicate keys
when the same file_id appears twice in one bucket (rare but
possible — a tool call writing the same path twice). Without
unique keys, React's reconciler could reuse the wrong card
instance, undermining the latest-mention dedup.
comprehensive review NITs:
- Attachment.tsx: hoist `import type { ToolArtifactType }` up into
the type-import section per AGENTS.md.
- artifacts.ts `fileToArtifact`: defense-in-depth empty-text guard
for the `preClassifiedType` path. Mirrors the gate in
`detectArtifactTypeFromFile` so a future caller that bypasses
classification can't hand sandpack/mermaid an empty buffer.
Plain-text and markdown remain tolerated empty.
Tests:
- New: empty `.md` content passes through unchanged when a
placeholder is also supplied.
- New: sibling cards with the same file_id in one group render
without React key-collision warnings.
- Updated existing placeholder test to use `text: null` (the case
where the placeholder is actually meant to fire).
- Three parameterized cases pinning the new
preClassifiedType-with-empty-text safety guard.
* 🩹 fix: Address codex P1/P2 review on code-artifacts-panel
- P1 (stale artifacts leak across conversations): Add a top-level
`useResetArtifactsOnConversationChange` hook in `Presentation.tsx`
that wipes `artifactsState` / `currentArtifactId` on every
conversation switch, regardless of panel visibility. Without this
guard, ToolArtifactCard's self-heal effect would re-register the
previous conversation's artifacts after panel close, leaking them
into the next conversation's panel on open.
- P2 (expiresAt skipped on panel-routed entries): Restore the legacy
expiry gate in `LogContent` ahead of panel/mermaid bucket-sort, so
expired pptx/html/etc. attachments fall back to the
"download expired" message instead of rendering as a clickable
artifact card backed by a dead link.
Includes regression coverage for both paths.
* 🧹 chore: Share renderAttachmentKey across Attachment + LogContent
Hoist the per-occurrence React-key helper from `Attachment.tsx` into
`attachmentTypes.ts` so `LogContent` can use the same pattern. Apply
it to LogContent's panel/mermaid/text/image/nonInline buckets — the
prior keys (e.g. `mermaid-${file_id ?? index}`, `file.filepath ?? ...`)
would have collided if the same file_id appeared twice in one render,
even though that's astronomically rare for a single tool call.
Also drops the unused `file_id` field on `MermaidEntry` since the key
no longer needs it.
* 🩹 fix: Loosen artifacts util input types to match runtime fallbacks
`fileToArtifact`, `detectArtifactTypeFromFile`, `toolArtifactKey`, and
`toLastUpdate` all read every picked field with a nullish fallback —
their inputs were nonetheless typed as required `Pick<TFile, ...>`.
That mismatch made every realistic fixture (and several call sites
that lack a stable `filepath`) fail typecheck for fields the
implementations never strictly need.
Wrap the picks in `Partial<>` so the type matches the contract.
* 🩹 fix: Gate tool-artifact registration on claim winner
When two `ToolArtifactCard` instances mount for the same `artifact.id`
with divergent content (a code-execution file overwritten across
turns reuses its file_id), both effects subscribe to `existingEntry`
through `artifactByIdSelector`. Each card detects the other's write as
drift and overwrites it back, ping-ponging `artifactsState` between
old and new content and causing render churn / panel flicker.
Gate the self-heal registration on `isMyClaim` so only the latest
(claim-holding) card writes. The non-winner still subscribes to the
slice but short-circuits before calling `setArtifacts`, breaking the
loop. Adds a regression test that fails (loop / wrong final content)
without the gate.
* 🌱 fix: Seed Code Tool Files Into Graph Sessions on First Call
Files attached to an agent's `tool_resources.execute_code` (user uploads
or generated artifacts from a prior turn) were silently dropped on the
first `execute_code` invocation of a turn. The agents-side `ToolNode`
populates `_injected_files` only when its `sessions` map already has an
`EXECUTE_CODE` entry — but that entry is only written by a previous
successful execution, so call #1 had nothing to inject. CodeExecutor
then fell back to a `/files/{session_id}` fetch, but `session_id` was
also empty on call #1, leaving the sandbox without the primed files.
Mirror the existing skill-priming pattern (`primeInvokedSkills` →
`initialSessions`) for code-resource files: eagerly call `primeFiles`
before `createRun` and merge the result into `initialSessions` via a
new `seedCodeFilesIntoSessions` helper. Skill files and code-resource
files now share the same `EXECUTE_CODE` entry; the prior representative
`session_id` is preserved on merge.
* 🔬 chore: Add Diagnostic Logging for Code-Files Seeding
Temporary debug logs to diagnose why first-call file injection is not
firing in real agent runs. Logs `wantsCodeExec`, available tool-resource
keys, primed file count, and the seeded EXECUTE_CODE entry. Will revert
once the failure mode is identified.
* 🪛 refactor: Capture primedCodeFiles per-agent at init, merge across run
Replace the client.js eager `primeFiles` call with a per-agent capture at
initialization time so every agent in a multi-agent run (primary +
handoff + addedConvo) contributes its `tool_resources.execute_code`
files to the shared `Graph.sessions` seed.
- handleTools.js (eager loadTools): the `execute_code` factory closes
over a `primedCodeFiles` slot and surfaces it in the return.
- ToolService.js loadToolDefinitionsWrapper (event-driven): captures
`files` from the existing `primeCodeFiles` call (was dropping them
while only keeping `toolContext`) and surfaces them.
- packages/api initialize.ts: the loadTools callback contract now
includes `primedCodeFiles`, threaded onto `InitializedAgent`.
- client.js: iterate `[primary, ...agentConfigs.values()]` and merge
each agent's `primedCodeFiles` into `initialSessions`. Drop the
primary-only `primeCodeFiles` call and diagnostic logs from the prior
attempt — wrong layer (single-agent), wrong gate (`agent.tools`
contained Tool instances after init, so the `.includes("execute_code")`
string check always failed).
* 🔬 chore: Add per-agent diagnostic logs for code-files seeding
Logs `tool_resources` keys + file counts inside loadToolDefinitionsWrapper
and per-agent `primedCodeFiles` + final initialSessions inside
AgentClient. Will revert once the failure mode is confirmed.
* 🔬 chore: Add file-lookup diagnostics inside initializeAgent
Logs the inputs and intermediate counts of the conversation-file lookup
chain (convo file ids, thread message ids, code-generated and
user-code file counts) so we can pinpoint why `tool_resources.execute_code`
is arriving empty at `loadToolDefinitionsWrapper` despite the agent
having `execute_code` in its tools list.
* 🔬 chore: Probe execute_code files without messageId filter
Adds a relaxed `getFiles({conversationId, context: execute_code})` probe
that runs only when `getCodeGeneratedFiles` returns empty. Lists what's
actually in the DB for this conversation so we can confirm whether the
file is missing entirely or whether the messageId filter is rejecting it.
* 🔬 chore: Fix probe getFiles arg order (sort vs projection)
Probe was passing a projection object as the sort arg, which mongoose
rejected with `Invalid sort value`. Move it to the third arg
(selectFields) so the probe actually runs.
* 🪢 fix: Preserve Original messageId on Code-Output File Update
Each `processCodeOutput` call was overwriting the persisted file's
`messageId` with the *current* run's id. When a turn re-creates an
existing file (filename + conversationId match → `claimCodeFile`
returns the existing record, `isUpdate=true`), the file's link to
the assistant message that originally produced it gets clobbered.
`initializeAgent` later runs `getCodeGeneratedFiles({messageId: $in: <thread>})`
to seed `tool_resources.execute_code` from prior-turn artifacts. With a
stale `messageId` (e.g. from a failed read attempt that re-shelled the
same filename), the file no longer matches the parent-walk thread, so
`tool_resources` arrives empty at agent init, the new
`primedCodeFiles` channel has nothing to seed, and the LLM can't see
its own prior-turn artifacts on the next turn — defeating the
just-added Graph-sessions seeding fix.
Preserve the existing `claimed.messageId` on update; first-creation
behavior is unchanged. The runtime return value still includes the
current run's `messageId` (via `Object.assign(file, { messageId })`)
so the artifact is correctly attributed to the live tool_call.
* 🧹 chore: Remove diagnostic logs from code-files seeding path
Drops the temporary debug logs added to trace the empty-tool_resources
failure mode. Production code paths (loadToolDefinitionsWrapper,
client.js seed loop, initializeAgent file lookup) are left as the
permanent shape: capture primedCodeFiles, merge across agents, seed
initialSessions before run start.
* 🪛 feat: read_file Sandbox Fallback for /mnt/data + Non-Skill Paths
When the model called `read_file` with a code-execution path (e.g.
`/mnt/data/sentinel.txt`), the handler returned a misleading
`Use format: {skillName}/{path}` error. Adds a sandbox-aware fallback:
- Short-circuit `/mnt/data/...` (can never be a skill reference) →
route to a sandbox `cat` via the new host-provided `readSandboxFile`
callback, which POSTs to the codeapi `/exec` endpoint.
- Skip the skill resolver entirely when `accessibleSkillIds` is empty
— the resolved-output of `resolveAgentScopedSkillIds` already
collapses the admin capability + ephemeral badge + persisted
`skills_enabled` chain, so an empty value is the authoritative
"skills aren't in scope for this agent" signal.
- For `{firstSegment}/...` paths, consult the catalog-derived
`activeSkillNames` Set (no DB read) to detect non-skill names and
fall through to the sandbox before the model has to retry with
`bash_tool`.
`activeSkillNames` is captured from `injectSkillCatalog`, threaded onto
`InitializedAgent`, into `agentToolContexts`, then through
`enrichWithSkillConfigurable` into `mergedConfigurable` for the handler.
The host implementation of `readSandboxFile` lives in
`api/server/services/Files/Code/process.js` and shells `cat <path>`
through the seeded sandbox session — `tc.codeSessionContext`
(emitted by ToolNode for `read_file` calls in `@librechat/agents`
v3.1.72+) provides the `session_id` + `_injected_files` so the read
lands in the same sandbox that holds prior-turn artifacts. When the
seeded context isn't available (older agents version, no codeapi
configured), the handler returns a model-visible error pointing at
`bash_tool` instead of silently failing.
Tests: 8 new `handleReadFileCall` cases cover the new short-circuits,
the skills-not-enabled gate, the activeSkillNames lookup, the
sandbox-fallback success path, and the bash_tool retry hint on
fallback failure. Existing `read_file` tests now opt into "skills are
in scope" via a `skillsInScope()` fixture (production wouldn't reach
the skill lookup with empty `accessibleSkillIds`).
* 🔧 chore: Update @librechat/agents dependency to version 3.1.72
Bumps the version of the @librechat/agents package across package-lock.json and relevant package.json files to ensure compatibility with the latest features and fixes.
* 🪛 refactor: Centralize Tool-Session Seed in buildInitialToolSessions Helper
Addresses review feedback on the per-agent merge in client.js:
- **Run-wide semantics, named explicitly.** The merge into a single
`Graph.sessions[EXECUTE_CODE]` was a deliberate match to the
agents-library design (`Graph.sessions` is shared across every
`ToolNode` in the run), but the inline `for (const a of agents)`
loop in `AgentClient.chatCompletion` made it look per-agent. Move
the logic to a TS helper `buildInitialToolSessions` that documents
the run-wide-by-design contract in one place. The CJS controller
now contains a single call site, no business logic.
- **Subagent walk (P2).** The previous loop only iterated
`[primary, ...agentConfigs.values()]`. Pure subagents are pruned
out of `agentConfigs` after init and retained on each parent's
`subagentAgentConfigs`, so their primed code files were silently
dropped from the seed. The helper now walks recursively, with a
visited-Set keyed on object identity that terminates safely on a
malformed agent graph (cycle).
- **`jest.setup.cjs` polyfill for undici `File`.** Reviewer hit
`ReferenceError: File is not defined` running the targeted spec on
WSL — a known Node 18 issue where `globalThis.File` from
`node:buffer` isn't auto-exposed. Polyfill it inside a Jest setup
file so the suite boots regardless of Node patch version.
Helper test coverage (8 new): skill-only / agent-only / both,
recursive subagent walk, cycle-safe walk, primary+subagent
deduplication, undefined/null entries in the agents iterable, and
representative session_id preservation across the merge.
16 tests pass total in `codeFilesSession.spec.ts` (8 prior + 8 new).
No behavior change vs. the previous commit for the existing
primary+agentConfigs case — subagent inclusion is the only new
behavior, and it matches what the existing seeding logic would have
done if subagents had been in `agentConfigs`.
* 🪛 fix: FIFO Walk Order in buildInitialToolSessions (P3 review)
The traversal used `Array.pop()` (LIFO), which visited the LAST
top-level agent first. The docstring says "primary first"; the code
contradicted it. When no skill seed exists the first-visited agent's
first file supplies the representative `session_id` written to
`Graph.sessions[EXECUTE_CODE]` — so a LIFO walk silently flipped which
agent that came from. `ToolNode` ultimately uses per-file `session_id`s
for runtime injection (so behavior was indistinguishable for current
callers), but the discrepancy was a footgun for any future consumer
that read the representative.
Switch to FIFO via `Array.shift()` to match both the docstring and the
existing `loadSubagentsFor` walk pattern in
`Endpoints/agents/initialize.js`. Add a regression test that asserts
the primary's `session_id` is the representative (and that all three
agents' files still contribute, with per-file `session_id`s preserved).
* 🔬 test: Lock In Code-Files Bug Fixes Per Comprehensive Review
Addresses MAJOR + MINOR + NIT findings from the multi-pass review:
**Finding #4 (MINOR) — empty relativePath misses sandbox fallback.**
A model calling `read_file("output/")` where "output" isn't a skill
name dead-ended with `Missing file path after skill name` instead of
being routed to the sandbox like every other malformed-path branch.
Add the same `codeEnvAvailable → handleSandboxFileFallback` pattern,
plus two regression tests.
**Finding #7 (NIT) — duplicate `skillsInScope()` helper.**
Hoist the identical helper out of two nested describe blocks to
module scope. Single source of truth.
**Finding #1 (MAJOR) — `persistedMessageId` had zero test coverage.**
The fix preserves a file's original `messageId` on update so
`getCodeGeneratedFiles` can still match it on subsequent turns. A
regression in the `isUpdate ? (claimed.messageId ?? messageId) : messageId`
ternary would silently re-introduce the original cross-turn priming
bug. Five new tests cover:
- UPDATE preserves `claimed.messageId` in the persisted record
- UPDATE falls back to current run id when `claimed.messageId` is
absent (legacy records predating the field)
- CREATE uses current run id (no claimed record exists)
- The runtime return value uses the LIVE id (artifact attribution)
even when the persisted record kept the original
- The image branch follows the same contract (would silently regress
if the ternary diverged across the two file-build branches)
The tests use a `snapshotCreateFileArgs()` helper because
`processCodeOutput` mutates the file object after `createFile`
returns (`Object.assign(file, { messageId, toolCallId })`) and a
naive `createFile.mock.calls[0][0]` would reflect the post-mutation
state instead of what was actually persisted.
**Finding #2 (MAJOR) — `readSandboxFile` had no direct tests.**
The model-controlled `file_path` flows through a POSIX single-quote
escape into a shell `cat` command, making this a security boundary.
A quoting regression would let a malicious filename break out of the
quoted argument and inject arbitrary shell. 20 new tests across:
- Shell quoting (7): plain filenames, embedded `'`, `$()`, backticks,
newlines, shell metachars, multiple consecutive single-quotes
- Payload shape (6): /exec URL, bash language, conditional
session_id / files inclusion, dedicated keepAlive:false agents
- Response handling (6): `{content}` on success, null on missing
base URL or absent stdout, throws on stderr-only, partial-success
returns stdout, transport errors are logged then rethrown
- Timeout (1): matches processCodeOutput's 15s SLA
Audited findings #5 (acknowledged tech debt — readSandboxFile in JS
workspace), #6 (pre-existing positional-args debt on
enrichWithSkillConfigurable), and #8 (cosmetic JSDoc style) — no
action taken per the reviewer's own assessment.
Audited finding #3 (walk order vs docstring) — already addressed in
commit 007f32341 which converted to FIFO via `queue.shift()` plus a
regression test. The audit was performed against an earlier PR head.
Tests: 152 packages/api + 195 api JS = 347 pass. Typecheck clean.
* 🪛 fix: Pure-Subagent codeEnv + Primed-Skill Routing + ToolService Early Returns
Three findings from the second-pass review:
**P2 — Pure subagents missed `codeEnvAvailable`** (initialize.js).
The pure-subagent init path didn't forward the endpoint-level
`codeEnvAvailable` flag to `initializeAgent`, unlike the primary,
handoff, and addedConvo paths. A code-enabled subagent loaded only
through `subagentAgentConfigs` initialized with
`codeEnvAvailable: false`, so even though the recursive seed walk
found its primed code files, the subagent's own `bash_tool` /
`read_file` sandbox fallback were silently gated off. Forward the
flag and add `codeEnvAvailable: config.codeEnvAvailable` to the
`agentToolContexts.set` for symmetry with the other paths.
**P2 — Primed skills outside the catalog cap were misrouted to
sandbox** (handlers.ts). Manual ($-popover) and always-apply primes
are intentionally resolved off the wider `accessibleSkillIds` ACL
set BEFORE catalog injection — see `resolveManualSkills` for why a
skill outside the `SKILL_CATALOG_LIMIT` cap can still be authorized
for direct manual invocation. The `activeSkillNames` shortcut ran
before reading `skillPrimedIdsByName`, so a primed skill not in the
catalog would fall through to the sandbox instead of resolving via
the pinned `_id`. Read the primed map first and bypass the shortcut
for primed names. New regression test asserts a primed-but-not-
cataloged skill resolves through the existing skill path with
`getSkillByName` invoked and `readSandboxFile` NOT called.
**P3 — `loadAgentTools` early returns dropped `primedCodeFiles`**
(ToolService.js). The non-`definitionsOnly` path captures the field
correctly, but two early-return branches (no-action-tools fast path,
no-action-sets fast path) omitted it. Any traditional
`loadAgentTools(..., definitionsOnly: false)` caller using
execute_code without action tools would have its first-call session
seed silently empty. Add `primedCodeFiles` to both early returns
for consistency with the final return shape.
Tests: 153 packages/api + 195 api JS = 348 pass.
* 🧹 chore: Document jest.mock arrow-indirection pattern in process.spec.js
Per the second-pass review's Finding #2 (NIT, "would help future
readers"): the mock setup mixes direct `jest.fn()` references with
arrow-function indirection (`(...args) => mockX(...args)`). The
indirection isn't stylistic — it's required because `jest.mock(...)`
is hoisted above the outer `const` declarations at parse time, so a
direct reference would capture `undefined`. Inline comment explains
the pattern so the next reader doesn't have to reverse-engineer it
or accidentally "simplify" the mocks and break per-test
`mockReturnValueOnce` / `mockImplementationOnce` overrides.
* 🪛 fix: Five Issues from Pass-N + Codex Review (incl. 404 root cause)
Five real bugs surfaced by another review pass + Codex PR comments
+ the codeapi-side logs we collected during manual testing:
**1) `processCodeOutput` 404 root cause (`callbacks.js`).**
The codeapi worker emits TWO distinct `session_id`s on a tool result:
- `artifact.session_id` is the EXEC session — the sandbox VM that
ran the bash command. Files don't live there; it's torn down
post-execution.
- `file.session_id` is the STORAGE session — the file-server
bucket prefix where artifacts actually live.
`callbacks.js` was passing the EXEC id to `processCodeOutput`, which
builds `/download/{session_id}/{id}` and 404s because the file-server
doesn't know about that path. This explains every "Error
downloading/processing code environment file" we saw during testing.
Use `file.session_id ?? output.artifact.session_id` (per-file id with
artifact-level fallback for older worker payloads).
**2) `primeFiles` reupload pushed STALE sandbox ids (`process.js`).**
When `getSessionInfo` returns null (file expired/missing in sandbox),
`reuploadFile` re-uploads via `handleFileUpload`, gets a NEW
`fileIdentifier`, and persists it on the DB record. But `pushFile`
was a closure capturing the OLD `(session_id, id)` parsed at the top
of the loop, so the in-memory `files[]` array (now consumed by
`buildInitialToolSessions` to seed `Graph.sessions`) silently
referenced a sandbox object that no longer existed. The first tool
call would 404 trying to mount it; only the next turn's metadata
re-read would correct course. Parameterize `pushFile` with optional
`(session_id, id)` overrides; in `reuploadFile` parse the new
identifier and pass through. 2 regression tests.
**3) Codex P2 — Cap sandbox fallback output before line-numbering
(`handlers.ts`).** The new `handleSandboxFileFallback` returned
`addLineNumbers(result.content)` without a size guard, so reading a
multi-MB `/mnt/data/*` artifact materialized the file twice in
memory (raw + line-numbered) before downstream truncation. Match the
existing skill-file path's `MAX_READABLE_BYTES` (256KB): truncate
raw first, then number, surface the truncation to the model so it
can use `bash_tool` (`head` / `tail`) for the rest. 2 tests
(oversized truncates with hint, in-cap doesn't).
**4) Codex P2 — Dedupe seeded code files by `(session_id, id)`
(`codeFilesSession.ts`).** Multiple agents in a run commonly carry
the same primed execute-code resources (shared conversation files);
without dedupe, `_injected_files` grows proportionally to agent
count and bloats every `/exec` POST. Use a `(session_id, id)`
identity key so first-seen wins (preserves source ordering); name
alone isn't sufficient because two distinct primed uploads can
share a filename across different sessions. 4 tests covering dedup
across iterations, against pre-existing entries, name-collision
distinct-session preservation, and the multi-agent realistic case
in `buildInitialToolSessions`.
**5) Pass-N P2 — Polyfill `globalThis.File` in api Jest setup
(`api/test/jestSetup.js`).** `packages/api/jest.setup.cjs` had the
polyfill; the legacy api workspace's Jest config has its own
`setupFiles` that didn't, so on Node 18 / WSL the api focused tests
still failed at import time with `ReferenceError: File is not
defined` from undici. Mirror the polyfill.
Tests: 159 packages/api + 206 api JS = 365 pass. Typecheck clean.
* 🔧 chore: Update @librechat/agents dependency to version 3.1.73
Bumps the version of the @librechat/agents package across package-lock.json and relevant package.json files to ensure compatibility with the latest features and fixes.
* chore: Update `@librechat/agents` to v3.1.71-dev.0 across package-lock and package.json files
This commit updates the version of the `@librechat/agents` package from `3.1.70` to `3.1.71-dev.0` in the `package-lock.json` and relevant `package.json` files. Additionally, it marks several dependencies as peer dependencies, ensuring better compatibility and integration across the project.
* 🔗 feat: Enable Tool-Output References for bash_tool when codeenv is on
Wires `@librechat/agents`' `RunConfig.toolOutputReferences` into
`createRun()` and the bash tool's LLM-facing description, gated by the
per-agent `effectiveCodeEnvAvailable` flag. The feature auto-activates
for any run where the bash tool is actually registered; SDK defaults
(~400 KB per output, 5 MB total) match the shell-safe budget. No new
env var or yaml capability — piggybacks on the existing `execute_code`
gate.
- `tools.ts`: replace the module-level `BASH_TOOL_DEF` constant with a
per-call `buildBashToolDef` that wraps `buildBashExecutionToolDescription`.
Description now includes the `{{tool<idx>turn<turn>}}` reference syntax
guide iff the new `enableToolOutputReferences` param is true.
- `initialize.ts`: pass `enableToolOutputReferences: effectiveCodeEnvAvailable`
into `registerCodeExecutionTools`.
- `run.ts`: add `codeEnvAvailable?: boolean` to `RunAgent`, compute the
flag from `agents[*].codeEnvAvailable`, and conditionally spread
`toolOutputReferences: { enabled: true }` into `Run.create`.
* 🧪 test: Cover tool-output references gating end-to-end
- `tools.spec.ts`: 3 new cases asserting `bash_tool.description`
contains `{{tool<idx>turn<turn>}}` iff `enableToolOutputReferences` is
true (and unset → false).
- `run-summarization.test.ts`: 4 new cases asserting `Run.create` is
invoked with `toolOutputReferences: { enabled: true }` iff at least
one `RunAgent.codeEnvAvailable === true`. Covers the present /
absent / unset / multi-agent-OR cases.
- `initialize.test.ts` + `skills.test.ts`: extend the existing
`@librechat/agents` jest mocks with a `buildBashExecutionToolDescription`
stub so suites stay green when the on-disk SDK lags the published
3.1.71-dev.0 export.
* chore: Update `@librechat/agents` version to `3.1.71-dev.1` in package-lock and package.json files
This commit updates the version of the `@librechat/agents` package from `3.1.71-dev.0` to `3.1.71-dev.1` across the relevant package files. This change ensures consistency and incorporates any updates or fixes from the new version.
* 🪢 fix: Walk Subagents in toolOutputReferences run-level gate
Codex P2 review on PR #12830: the run-level
`enableToolOutputReferences` flag only inspected the top-level
`agents` array. A parent agent without `execute_code` that spawns a
subagent that *does* have it left the SDK's tool-output reference
registry inactive for the run, so the subagent's `bash_tool` calls
saw `{{tool<idx>turn<turn>}}` placeholders pass through to the
shell unsubstituted.
Replace `agents.some(a => a.codeEnvAvailable === true)` with a
recursive `anyAgentHasCodeEnv` helper that walks
`subagentAgentConfigs` transitively. Cycle-safe via a `visited` set,
mirroring the existing `buildSubagentConfigs.ancestors` pattern in
the same module. The bash tool *description* stays per-agent in
`initializeAgent` (only agents with bash actually registered learn
the `{{…}}` syntax), so broadening the run-level gate doesn't
broaden the model-facing surface — it just lets the SDK's shared
registry serve every `ToolNode` the run compiles, which is exactly
the contract the SDK already implements.
Tests cover three new cases: parent-off / subagent-on, parent-off /
child-off / grandchild-on (transitive descent past one level), and
a cyclic A↔B tree with neither codeenv-enabled (asserts both
termination and absence of `toolOutputReferences`). Existing
single-agent and multi-agent tests stay valid since the new helper
returns `true` whenever the previous `.some(...)` did.
* chore: Update `@librechat/agents` version to `3.1.71` in package-lock and package.json files
This commit updates the version of the `@librechat/agents` package from `3.1.71-dev.1` to `3.1.71` across the relevant package files. This change ensures consistency and incorporates any updates or fixes from the stable release.
* review: address audit findings on tool-output references PR
Two findings from comprehensive PR review on #12830:
#1 (MINOR) — `injectSkillCatalog` omitted `enableToolOutputReferences`
when calling `registerCodeExecutionTools`, so its resulting
`bash_tool` description always lacked the `{{tool<idx>turn<turn>}}`
guide. Today this is a no-op because `initializeAgent` registers
first and the registry `.has()` check makes the skills-path call a
dedupe-only operation. But if call order ever flips (skills-first),
the missing flag would silently ship a `bash_tool` without the
syntax guide, and the `initializeAgent` pass would itself become
the no-op — the feature would silently break with no visible error.
Forward `enableToolOutputReferences: codeEnvAvailable === true` so
both call sites produce identical tool definitions regardless of
firing order. Defense-in-depth, not a current bug. Added a test in
`skills.test.ts` that asserts the bash description contains the
`{{tool<idx>turn<turn>}}` marker when `codeEnvAvailable` is on,
exercising the skills caller end-to-end.
#2 (NIT) — `buildBashToolDef` allocated + froze a fresh object on
every agent init. Replaced with two module-level frozen singletons
(`BASH_TOOL_DEF_WITH_OUTPUT_REFS`, `BASH_TOOL_DEF_WITHOUT_OUTPUT_REFS`)
built once at module load via a `createBashToolDef` helper. The
factory now picks the right cached reference instead of building.
Restores the no-allocation intent of the original `BASH_TOOL_DEF`
constant while keeping the per-agent gate behavior. Two new tests
in `tools.spec.ts` pin the contract: identical-flag calls return
reference-equal `bash_tool` defs across registries; opposite-flag
calls return distinct frozen objects with the expected description
content.
* 📄 feat: Auto-render Text-Based Code Execution Artifacts Inline
Eagerly extract text content from non-image artifacts produced by code
execution tools and render it inline in the message instead of behind a
click-to-download file card. Reuses the SkillFiles binary-detection
helper and the existing parseDocument dispatcher so docx, xlsx, csv,
html, code, and other text-renderable formats land directly under the
tool call.
PPTX is intentionally classified but not yet extracted — follow-up.
* 🌐 chore: Remove unused com_download_expires locale key
Removed in en/translation.json so the detect-unused-i18n-keys CI check
passes. The only reference was a commented-out localize() call in
LogContent.tsx that was deleted in the previous commit.
* 🩹 fix: Address PR review on code artifact text extraction
- extract.ts: build the temp document path from a randomUUID and pass
path.basename(name) as originalname so a malicious artifact name
cannot escape os.tmpdir() (P1 traversal flagged by codex/Copilot).
- process.js: classify and extract using safeName, not the raw name —
defense in depth alongside the temp-path fix.
- classify.ts: add a bare-name lookup so extensionless text artifacts
(Makefile, Dockerfile, …) classify as utf8-text instead of falling
through to other.
- Attachment.tsx: wire aria-expanded / aria-controls on the show-all
toggle for screen reader support.
- LogContent.tsx: restore a download chip (LogLink) on inline-text
attachments so users can still pull down the underlying file.
- Tests: cover extensionless filenames and the temp-path traversal
invariant.
* 🩹 fix: Address comprehensive PR review on code artifact extraction
- extract.ts: walk back to a UTF-8 code-point boundary before truncating
so cuts cannot land mid-multibyte and emit U+FFFD (CJK/emoji concern).
truncate() now accepts the original buffer to skip a redundant encode.
- extract.ts: add an 8s timeout around parseDocument via Promise.race so
a pathological docx/xlsx cannot stall the response path.
- process.js: always set `text` (string or null) on the file payload —
createFile uses findOneAndUpdate with $set semantics, so omitting the
field leaves a stale value behind when an artifact's content changes.
- Attachment.tsx: switch the show-all toggle from char-count threshold
to a useLayoutEffect ref measurement on scrollHeight, and use
overflow-hidden when collapsed (overflow-auto when expanded) so the
collapsed box has a single clear interaction model.
- Attachment.tsx + LogContent.tsx: lift `isImageAttachment` /
`isTextAttachment` into a shared attachmentTypes module. LogContent
keeps its looser image check (no width/height required) because the
legacy log surface receives attachments without dimensions.
- Tests: cover multi-byte boundary, the always-set-text contract on
updates, and the new shared predicates.
* 🧪 test: Component test for TextAttachment + direct withTimeout coverage
- Attachment.tsx: re-order local imports longest-to-shortest per
AGENTS.md (attachmentTypes ahead of FileContainer/Image).
- extract.ts: export withTimeout so it can be unit-tested directly
(it's also used internally — exporting carries no runtime cost).
- extract.spec.ts: three small unit tests on withTimeout that cover
resolve, propagated rejection, and timeout rejection paths with
real timers.
- TextAttachment.test.tsx: ten cases for the new React component —
text rendering in <pre>, download chip presence/absence, ref-based
collapse measurement (with scrollHeight stubbed via prototype),
aria-expanded toggle, fall-through to FileAttachment for missing
and empty text, and AttachmentGroup routing.
* 🩹 fix: Canonicalize document MIME by extension before parseDocument
When the classifier puts a file on the document path via its extension
(.docx, .xlsx, …) but the buffer sniffer returned a generic value like
application/zip or application/octet-stream, we previously forwarded
that generic MIME to parseDocument, which dispatches strictly by MIME
and silently rejected it — exactly defeating the extension-first
classification this PR added.
extractDocument now remaps the MIME from the extension (falling back
to the original sniffed MIME if the extension is unrecognized, so files
that reached the document branch via MIME detection still work). Adds
a parameterized test across docx/xlsx/xls/ods/odt against zip/octet
sniffs to guard the regression.
* 🩹 fix: Reuse existing withTimeout from utils/promise
The previous commit's local withTimeout export collided with the
already-exported `withTimeout` from `~/utils/promise`, breaking the
@librechat/api tsc job (TS2308 ambiguous re-export).
Drops the duplicate, imports from `~/utils/promise`, and removes the
now-redundant unit tests (the helper has its own coverage in
utils/promise.spec.ts). The third argument shifts from a label to the
fully-formed timeout error message that the existing helper expects.
* 🧹 chore: TextAttachment test polish (NITs)
- Use the conventional `import Attachment, { AttachmentGroup }` form
rather than `default as Attachment`.
- Save the original `scrollHeight` property descriptor and restore it
in afterAll, so the prototype patch never leaks past this suite.
* 🛡️ fix: Strict opt-in skills activation per agent
Skills were activating on every agent run that had the capability +
RBAC enabled, regardless of whether the user (ephemeral) or author
(persisted) had opted in. `scopeSkillIds(undefined)` fell through to
"full accessible catalog" whenever `agent.skills` was unset, which is
the default state for any agent created before skills existed and for
every ephemeral agent.
Activation now requires an explicit signal:
- Ephemeral agent → per-conversation skills badge toggle.
- Persisted agent → new `skills_enabled` master switch on the agent
doc, surfaced as a toggle in the Agent Builder skills section.
Enabled + empty/undefined allowlist = full accessible catalog;
enabled + non-empty allowlist = narrow to those ids; disabled (or
undefined) = no skills available, even if an allowlist is set.
Centralised the predicate in `resolveAgentScopedSkillIds` so the
primary-agent path, handoff/discovery, the subagent loop, and both
OpenAI controllers all share one source of truth. Frontend `$`
popover scope mirrors the same logic so the UI never offers skills
the backend would refuse to activate.
* test: mock resolveAgentScopedSkillIds in agent controller specs
* refactor: address review findings on skills opt-in PR
- AgentConfig: associate skills label with toggle via htmlFor for
click/keyboard affordance; simplify Switch handler to Boolean(value).
- skills: mark scopeSkillIds as @internal so runtime callers continue
to route through resolveAgentScopedSkillIds and inherit the activation
predicate (ephemeral toggle, persisted skills_enabled).
* fix(agents): include skills_enabled in agent list projection
Without this field, agents loaded via the list endpoint hydrate into the
client agentsMap with skills_enabled === undefined, causing the `$`
skill popover to hide every skill on a fresh page load even when the
agent was saved with skills_enabled: true.
* fix(skills): fail closed for persisted agents during agentsMap hydration
Returning undefined while the agents map loads let the popover render the
full catalog for a persisted agent before we could read its
skills_enabled flag, so the user could pick a skill the backend would
then refuse for the turn. Match the strict opt-in contract by returning
[] until the map is authoritative.
* refactor(skills): extract skillsHintKey for readability
Replaces the nested ternary in the skills section JSX with a
pre-computed constant so the activation -> hint key mapping reads
top-down.
* refactor(skills): unflatten skillsHintKey to remove nested ternary
This commit updates the version of the `@librechat/agents` package from `3.1.68-dev.1` to `3.1.70` in the `package-lock.json` and relevant `package.json` files. This change ensures consistency across the project and incorporates any updates or fixes from the new version.
- Updated the Skills component to include a check for `skillsEnabled` from agent capabilities, ensuring skills are only displayed when both permissions and capabilities allow.
- Modified the `useHandleKeyUp` hook to prevent triggering skill commands when skills are disabled, enhancing user experience and preventing errors.
- Added tests to verify that skills commands do not trigger when skills are disabled, ensuring robust functionality.
- Refactored the `useSideNavLinks` hook to incorporate skills capability checks, ensuring consistent access control across the application.
* 🪆 feat: Subagents configuration (isolated-context child agents)
Surfaces the new @librechat/agents `SubagentConfig` primitive in the Agent
Builder. Subagents let a supervisor delegate a focused subtask to a child
graph running in an isolated context window: verbose tool output stays in
the child, only a filtered summary returns to the parent.
Data model: new `subagents: { enabled, allowSelf, agent_ids }` on Agent,
wired through the Zod, Mongoose, and form schemas plus a new
`AgentCapabilities.subagents` capability (enabled by default).
Backend: `initialize.js` loads explicit subagent configs alongside handoff
agents, and drops subagent-only references from the parallel/handoff maps
so they don't leak into the supervisor's graph. `run.ts` emits
`SubagentConfig[]` on the primary `AgentInputs` — a self-spawn entry when
`allowSelf` is enabled plus one entry per configured agent.
UI: an "Advanced" panel section with an enable toggle, a self-spawn
toggle, and an agent picker (capped at 10). Enabling without adding
agents still yields self-spawn; disabling self-spawn with no agents shows
a warning. A capability flag gates the whole section.
* 🪆 feat: Stream subagent progress to UI (dialog + inline ticker)
Pairs with the @librechat/agents SDK change that forwards child-graph
events through the parent's handler registry (danny-avila/agents#107):
- Self-spawn and explicit subagents can now use event-driven tools,
because child `ON_TOOL_EXECUTE` dispatches reach our ToolService via
the parent's registered handler.
- The same forwarding path wraps the child's run_step / run_step_delta
/ run_step_completed / message_delta / reasoning_delta dispatches in
a new `ON_SUBAGENT_UPDATE` envelope, with start/stop/error bookends.
Backend: `callbacks.js` registers an `ON_SUBAGENT_UPDATE` handler that
forwards each envelope straight to the SSE stream.
Frontend:
- `useStepHandler` consumes `ON_SUBAGENT_UPDATE` events and merges them
into a per-tool_call Recoil atom (`subagentProgressByToolCallId`).
First-seen `subagentRunId` claims the most-recent unclaimed `subagent`
tool call in the active response message — a temporal mapping, no SDK
wire-format change needed to correlate child runs with parent tool
calls.
- New `SubagentCall` part component replaces the default `ToolCall`
rendering when `toolCall.name === Constants.SUBAGENT`: compact status
ticker showing the 3 most recent update labels, clickable to open a
dialog with the full activity log + final markdown-rendered result.
- Adds `Constants.SUBAGENT`, `StepEvents.ON_SUBAGENT_UPDATE`, and
`SubagentUpdateEvent` type in data-provider.
Tests:
- `packages/api npx jest run-summarization` — 23 pass
- `api npx jest initialize` — 16 pass
- `npm run build` — clean
Dependency note: bumps `@librechat/agents` to `^3.1.67-dev.1` — requires
the SDK PR (danny-avila/agents#107) to be merged to dev and published
before this PR merges. `ON_SUBAGENT_UPDATE` is absent from dev.0, so the
handler registration would be a no-op with the older SDK but would not
crash.
* 🪆 fix: address Codex review and review audit on subagents
Stacks on top of the SDK change in danny-avila/agents#107 (bumped to
`^3.1.67-dev.2`).
- **P1 (`initialize.js`)**: subagent-only agents were being deleted from
both `agentConfigs` AND `agentToolContexts`. The tool-execute handler
resolves execution context (agent, tool_resources, skill ACLs) from
`agentToolContexts`, so explicit subagents would run without their
configured resources and skip action tools. Now only `agentConfigs`
is pruned — tool context stays intact.
- **P2 (`AgentSubagents.tsx`)**: toggling subagents off set the form
field to `undefined`; `removeNullishValues` stripped it from the
PATCH, leaving the server copy enabled. Now it persists an explicit
`{ enabled: false, ... }` so the update actually clears state.
- **Finding 1 (MAJOR)** — `agent_ids` Zod schema gains `.max()` via a
new `MAX_SUBAGENTS` export from `data-provider` (shared with the UI
cap). Crafted payloads can't trigger hundreds of `processAgent`
calls.
- **Finding 2 (MAJOR)** — `subagentProgressByToolCallId` atomFamily
atoms are now tracked in a ref and reset from `clearStepMaps` via a
`useRecoilCallback({ reset })`. No monotonic growth across a session.
- **Finding 3 (MAJOR)** — early-arriving `ON_SUBAGENT_UPDATE` events
whose parent `tool_call_id` is not yet mapped are now buffered in
`pendingSubagentBuffer` (keyed by `subagentRunId`) and replayed in
arrival order once correlation completes. Mirrors the existing
`pendingDeltaBuffer` pattern.
- **Finding 4 (MAJOR)** — switched to deterministic correlation via
the new `parentToolCallId` that SDK `3.1.67-dev.2` threads through
from `ToolRunnableConfig.toolCall.id`. Temporal fallback now iterates
oldest-unclaimed-first (forward), matching tool-call creation order,
so concurrent spawns map correctly.
- **Finding 6 (MINOR)** — `agent_ids` are deduped on the backend via
`new Set(...)` before the load loop. Duplicates no longer produce
duplicate `SubagentConfig` entries visible to the LLM.
- **Finding 7 (MINOR)** — events array inside each Recoil atom is
capped at 200 entries. Long-running subagents no longer replay O(n)
spreads on every update; the dialog log still shows the cap window.
- **Finding 8 (MINOR)** — documented: subagents are loaded only for
the primary agent this release (handoff children get self-spawn but
not explicit sub-subagents). In-code comment added so the next
maintainer doesn't wonder.
- **Finding 9 (NIT)** — removed `{!isSubmitting && null}` dead code
and the misleading announce-polite comment in `SubagentCall`.
- New `validation.spec.ts` — 9 tests covering the cap on
`agent_ids.length` at the subagent schema, agent-create, and
agent-update layers.
- `run-summarization` — 23 pass, `initialize` — 16 pass, total backend
package: 103 pass across touched areas.
Findings 5 (component tests) and 10 (micro-allocation) are tracked
but deferred; the former needs a Recoil-RenderHook harness that isn't
in this PR's scope, and the latter has negligible impact (one `Array.from`
per subagent run).
* 🧪 test: integration coverage for subagent correlation + backend loading
Addresses the follow-up audit on #12725 with real-code tests (no mock
handlers, only the existing setMessages/getMessages spies and the
standard mongodb-memory-server harness).
Six new tests under a dedicated `describe('subagent loading')`:
- loads a configured subagent, populates `subagentAgentConfigs`, keeps
it out of `agentConfigs`
- **P1 regression guard**: drives the real `toolExecuteOptions.loadTools`
closure with the subagent id and asserts `loadToolsForExecution` is
called with `agent: <subagent>`, `tool_resources`, `actionsEnabled`.
If anyone deletes `agentToolContexts` again, this fails.
- dedup: three copies of the same id load the agent once
- overlap: agent referenced both as handoff target and subagent stays in
`agentConfigs`
- capability gate: admin disabling `subagents` suppresses loading even
when the agent has a config
- per-agent disable: `subagents.enabled: false` skips loading entirely
Five new tests under `describe('on_subagent_update event')` using a
real `RecoilRoot` and a companion `useRecoilCallback` reader so writes
from the hook are observable:
- deterministic correlation via `parentToolCallId` (happy path with
SDK dev.2+)
- fallback: oldest-unclaimed tool call wins for concurrent spawns
without `parentToolCallId`
- early-arrival buffer: updates with no mapping get buffered and
replayed once the tool call appears
- event cap: 205 updates collapse to 200 retained, oldest dropped
- `clearStepMaps` resets tracked atoms back to their null default
- F2 — added explicit `// TODO` marker for handoff-subagent-loading
extension (matches the comment that referenced it).
- F3 — dropped the unnecessary `MAX_SUBAGENTS as MAX_SUBAGENTS_CAP`
alias; just import the constant directly.
- Bumped `@librechat/agents` to `^3.1.67-dev.3` to pick up the SDK's
paired test additions.
- `api/server/services/Endpoints/agents/initialize.spec.js` — 22 pass
(6 new + 16 existing)
- `packages/api/src/agents/validation.spec.ts` +
`run-summarization.test.ts` — 103 pass
- `client/src/hooks/SSE/__tests__/useStepHandler.spec.ts` — 48 pass
(5 new + 43 existing)
* 🪆 fix: strip parent run summary + discovered tools from subagent inputs
Codex P1 on #12725: `buildSubagentConfigs` reused the shared
`buildAgentInput` factory for each explicit child, and that factory
always stamps the parent run's `initialSummary` (cross-run conversation
summary) and `discoveredTools` (tool names the parent's LLM searched
earlier) onto every `AgentInputs` it returns. When subagents were
enabled on a conversation that had already been summarized, every
child inherited that summary — silently defeating the isolated-context
contract and burning extra tokens on unrelated prior chat.
Fix in `run.ts`: after `buildAgentInput(child)`, explicitly clear
`childInputs.initialSummary` and `childInputs.discoveredTools` before
attaching to the `SubagentConfig`. The parent keeps both — that's how
the supervisor receives cross-turn context — but the child starts
fresh.
Paired with danny-avila/agents#107 (bumped to `^3.1.67-dev.4`), which
adds the equivalent strip inside `buildChildInputs` to cover the
self-spawn path where the SDK clones parent `_sourceInputs` directly
and LibreChat never sees the intermediate shape. Belt and suspenders.
Regression test (new):
- `does NOT leak the parent run initialSummary into an explicit child
(Codex P1 regression)` — sets `initialSummary` on the run, enables
subagents with an explicit child, asserts the parent still has the
summary but `childConfig.agentInputs.initialSummary` is `undefined`.
Same for `discoveredTools`. 24 pass.
* 🪆 fix: capability gate applies to handoff agents + parallel subagent test
### Codex P2 — handoff agents kept `subagents` after capability disabled
The endpoint-level `AgentCapabilities.subagents` gate only cleared
`subagents` on `primaryConfig`. Handoff agents loaded into
`agentConfigs` retained their persisted `subagents.enabled: true`,
and because `run.ts` calls `buildSubagentConfigs` for every agent
input, self-spawn would still fire on a handoff target even when the
admin had disabled the capability globally.
Fix in `initialize.js`: after the subagent loading block, when the
capability is off, iterate `agentConfigs.values()` and clear
`subagents` + `subagentAgentConfigs` on every loaded config.
Regression test: `clears subagents on handoff agents too when
capability is disabled (Codex P2 regression)` — seeds a handoff target
with its own `subagents.enabled: true`, disables the capability at
the endpoint, asserts both primary AND handoff have `subagents`
undefined in the client args. 23 init tests pass.
### Parallel subagent correlation — user-requested verification
Added `keeps parallel subagent streams independent when events
interleave` to `useStepHandler.spec.ts`. Two `subagent` tool calls
seeded side by side, 6 interleaved `ON_SUBAGENT_UPDATE` envelopes
dispatched (a-start, b-start, a-step, b-step, a-stop, b-step), each
carrying its own `parentToolCallId`. Asserts each `tool_call_id`'s
Recoil bucket accumulates only its own run's events, statuses reflect
each run independently (`call_a` → stop, `call_b` → run_step), no
cross-contamination. 49 step-handler tests pass.
* 🪆 fix: SubagentCall detects cancelled / errored states (Codex P2)
Codex P2 on #12725: the old `running` check only consulted
`initialProgress` and the subagent's phase. A user stop, dropped
stream, or backend crash before a terminal `stop`/`error` envelope
arrived would leave the ticker permanently stuck on "working…". Other
*Call components (ToolCall.tsx) already model this via
`!isSubmitting && !finished` → cancelled.
Mirror that pattern. Re-introduce `isSubmitting` on `SubagentCallProps`
(the prop was dropped earlier as 'unused' — that was a bug) and resolve
status as a tri-state:
- `finished` — initialProgress >= 1, or subagent `stop`/`error`
- `cancelled` — `!isSubmitting && !finished`
- `running` — neither
New locale keys `com_ui_subagent_cancelled` + `com_ui_subagent_errored`
swap in the right header text per state.
Tests: new `SubagentCall.test.tsx` covers all four states with a real
`RecoilRoot` and a `useRecoilCallback` seeder — no mocked store — 5/5
pass. Includes an explicit P2 regression test that simulates the
`isSubmitting=false, progress.status='run_step', initialProgress<1`
scenario and asserts the cancelled label renders.
* 🪆 feat: semantic ticker + aggregated content-part dialog for subagents
Two rounds of feedback on #12725:
### Ticker — user-readable lines, not raw event names
The old ticker showed \`on_run_step\`, \`on_message_delta\`, etc. — not
meaningful to users. Replaced with \`buildSubagentTickerLines\`, a pure
helper that walks the \`SubagentUpdateEvent\` stream and emits:
- message/reasoning deltas → a single live "Writing: <last 60 chars>"
(or "Reasoning: …") line that updates in place as chunks arrive
- run_step with tool_calls → "Using calculator(expression=42*58)" for
a single call, "Using tool: a, b" for parallel (args dropped when
multiple so the line stays short)
- run_step_completed → "calculator → 42*58 = 2436" (output truncated
to 48 chars; falls back to "Tool X complete" when output is empty)
- error → "Error: <message>"
- start / stop / run_step_delta → suppressed (too granular / lifecycle-only)
Args and output pass through \`summarizeArgs\` / \`summarizeOutput\`
which flatten JSON to \`key=value\` pairs and head-truncate long
strings so a 200-line tool output never bloats the ticker.
### Dialog — aggregated content parts via leaf renderers
\`aggregateSubagentContent\` folds the raw event stream into
\`TMessageContentParts[]\` — text/reasoning delta streaks collapse into
single \`TEXT\` / \`THINK\` parts, tool calls become \`TOOL_CALL\` parts,
and \`run_step\` boundaries correctly break text runs around tool
calls. The dialog iterates those parts through a \`SubagentDialogPart\`
renderer that delegates to the existing \`Text\`, \`Reasoning\`, and
\`ToolCall\` leaf components — the same sub-components \`<Part />\` uses
— wrapped in a minimal \`MessageContext\` so reasoning expand state and
cursor animation work.
Leaf components are used directly rather than importing \`<Part />\`
itself to avoid a module cycle (Part → Parts/index → SubagentCall →
Part) and to sidestep a hypothetical nested-subagent rendering.
### Tests
- \`subagentContent.test.ts\` — 19 pure-function tests covering the
aggregator (text concat, reasoning concat, tool call lifecycle,
interleaving, phase suppression, late-arriving completions) and the
ticker builder (live preview truncation, args/output snippets,
parallel-call handling, output truncation, i18n formatter override).
- \`SubagentCall.test.tsx\` — 9 component tests: 5 status-resolution
(existing) + 2 ticker (semantic text, delta collapse) + 2 dialog
(aggregated parts routed to leaf renderers, raw-output fallback).
### Locale keys
New: \`com_ui_subagent_ticker_writing\`, \`…_reasoning\`, \`…_error\`,
\`…_using\`, \`…_using_with_args\`, \`…_tool_complete\`,
\`…_tool_output\`. Preserves i18n at the display layer while the
helper stays pure.
* chore: drop unused com_ui_subagent_activity_log locale key
The dialog no longer renders an "Activity log" section — the new
content-parts renderer replaced it. Also tweaks the dialog description
copy to match.
* 🪆 fix: subagent dialog order, persistence, auto-scroll, width
Follow-up pass addressing the four issues observed in real runs
against a live subagent-using parent.
### Aggregator ordering (reasoning appearing after text it preceded)
Reproducible pattern: LLM emits reasoning → text → tool call in that
order, but the dialog rendered text BEFORE reasoning in the content
array. Root cause: `aggregateSubagentContent` maintained `currentText`
and `currentThink` buffers in parallel and only flushed them at a
`run_step` boundary in a fixed (text, think) order, losing the actual
arrival order.
Fix: when a text chunk arrives, close any open think buffer first
(pushes it into the content array right then); symmetric for think →
text. Two new regression tests cover the exact reasoning → text →
tool_call sequence from the screenshot and the repeated
reasoning ↔ text flow across a turn.
### Content persists after completion (markdown not rendering when done)
`clearStepMaps` was calling `resetSubagentAtoms()` at stream end,
which wiped every `subagentProgressByToolCallId` entry. Once reset,
`contentParts.length === 0` and the dialog fell back to rendering the
raw `output` string with plain text — hence the literal `##`/`**` in
the completed-state screenshot. Stopped resetting; the atoms are
bounded per-call (200-event cap) and per-conversation (one per
subagent spawn) so growth matches the rest of the conversation state.
`resetSubagentAtoms` is kept for a future conversation-switch caller.
Also: routed the raw-`output` fallback (older subagent runs recorded
before the event forwarder existed) through the same
`SubagentDialogPart` → `Text` leaf that content parts use, so its
markdown renders the same way.
### Auto-scroll to bottom while running
Added a `scrollRef` on the dialog body and a `useEffect` that pins
`scrollTop = scrollHeight` while the dialog is open AND the subagent
is running. Triggers on `contentParts.length` (new tool calls / part
boundaries) and `events.length` (intra-part deltas) so the cursor
tracks text streaming. Disabled post-completion so re-opening a
finished run doesn't yank to the bottom.
### Wider dialog
Went from `max-w-2xl` (42rem / 672px — too cramped on maximized
laptop windows) to `w-[min(95vw,64rem)] max-w-[min(95vw,64rem)]`.
Narrow on phones, scales up to 64rem on desktop, always leaves a bit
of margin from the viewport edge. Bumped `max-h-[65vh]` on the scroll
area to give the extra width room to breathe vertically too.
### Tests
- `subagentContent.test.ts` — 21 pass (2 new ordering regressions).
- `useStepHandler.spec.ts` — 49 pass (1 updated to assert atoms are
*preserved* on clearStepMaps).
- `SubagentCall.test.tsx` — 9 pass (unchanged; aggregator-level tests
cover the ordering).
* 🪆 feat: persist subagent_content via SDK createContentAggregator
Per-request map of createContentAggregator instances keyed by the
parent's tool_call_id. ON_SUBAGENT_UPDATE handler feeds each event
into the matching aggregator (phase → GraphEvent mapping); AgentClient
harvests contentParts onto the subagent tool_call at message save so
the child's reasoning / tool calls / final text survive a page refresh.
Reusing the SDK's battle-tested aggregator instead of a bespoke one
keeps the persisted shape identical to the parent graph's output and
drops ~100 lines of custom aggregation code.
* 🪆 fix: incremental subagent aggregation + dialog render parity
**Disappearing tool_calls**: the Recoil atom trimmed events to a 200-long
rolling window, so verbose subagents could shed the `run_step` that
originally created a tool_call part — rebuilding content from the trimmed
window then produced only the surviving text/reasoning. Fix: fold each
envelope into `contentParts` incrementally in the atom as it arrives
(new `foldSubagentEvent` + cursor state). Event trim window now affects
only the ticker, never the dialog.
**Render parity**: dialog now applies `groupSequentialToolCalls` and
renders single parts through `Container` + grouped batches through
`ToolCallGroup` — same spacing and "Used N tools" collapsing the main
message view uses.
**Width**: `min(96vw, 80rem)` — wider on big screens, still responsive.
**Labels**: "Subagent: X" is jargon. Named subagents render as
`Running "{name}" agent` / `Ran "{name}" agent` (past tense on
completion); self-spawns use `Running subtask` / `Ran subtask` since
`Running "self" agent` reads badly.
* 🪆 polish: subagent dialog parity + agent avatar in header
**Labels**: drop "subtask" framing. Self-spawn shows `Running agent` /
`Ran agent` (past tense on completion); named subagents stay
`Running "X" agent` / `Ran "X" agent`.
**Dialog render parity**: stop wrapping every part in `Container`.
TEXT keeps its `Container` (gap-3 + `mt-5` sibling margin), THINK and
TOOL_CALL render bare so their own wrappers set the full-column width
the regular message view gives them — matches the main `<Part>` dispatch.
Outer scroll region now uses `px-4 py-3` padding and a
`max-w-full flex-grow flex-col gap-0` inner wrapper, mirroring the
`MessageParts` container the main conversation uses.
**Avatar**: header icon now renders the subagent's configured avatar
via `MessageIcon` when `useAgentsMapContext()` has the child agent,
falling back to the `Users` SVG (which keeps its running-state pulse).
Same icon-left-of-label pattern the tool UI uses.
* 🪆 polish: subagent group label, ticker throttle + tail-ellipsis, scroll button
**Grouped label**: ToolCallGroup now detects all-subagent batches and
labels them "Running N agents" / "Ran N agents" instead of "Used N
tools". Mixed batches keep the existing label. The tool-name summary
is suppressed for all-subagent groups (every entry dedupes to
"subagent", which adds nothing).
**Ticker width + tail-ellipsis**: raise the preview cap to 300 chars so
wide containers aren't half-empty, and flip the ticker `<li>` to
`dir="rtl"` so `text-overflow: ellipsis` clips the *oldest* characters
(visually the left edge) — the newest tokens stay pinned to the right
regardless of container width. Bidi lays out the Latin text LTR
internally, the rtl only affects which side gets the ellipsis.
**Throttle**: `useThrottledValue` hook (trailing-edge, 1.2s) smooths
the live `Writing: …` preview so tokens no longer strobe past the
eye faster than they can be read. Ref-based internals (not `useState`)
avoid infinite-update loops when the upstream value is a new-reference
each render; `NEGATIVE_INFINITY` sentinel ensures the very first value
passes through synchronously so tests and first paint aren't delayed.
**Scroll-to-bottom**: dialog tracks `isAtBottom` with a 120px
threshold; auto-scroll only engages when the user is already following
along, and a persistent jump-to-latest button appears whenever they
scroll up — no more fighting the auto-scroll to read back.
* 🪆 polish: snappier ticker, prefix-safe labels, agents icon, readable lines
**Ticker lines are now incrementally aggregated in the atom** — same
pattern as contentParts. The raw-events rolling window is gone; event
volume no longer caps what the ticker can display. Verbose subagents
that used to drop early tool_call lines out of the window now keep the
full 3-line history (using_tool, tool_complete, writing).
**Discriminated-union ticker lines** split a constant prefix (e.g.
"Writing:") from a tail-truncatable body. The prefix lives in a
`shrink-0` span so it never gets clipped when the body overflows; the
body uses `dir="rtl"` only on itself — scoped so non-streaming lines
(e.g. "Waiting for first update…") can't get their trailing ellipsis
flipped by bidi.
**Content-aware throttle**: 800ms interval (down from 1200ms), skipped
entirely while the live buffer is below 120 chars. Early tokens now
appear immediately — no more "Reasoning: I" sitting blank for a full
second before the next heartbeat. Once the preview is long enough to
fill the container, throttling kicks in at the tighter interval.
**Header label** is now a constant verb + optional muted sub-label.
Base reads "Running agent" / "Ran agent" / "Cancelled agent" / "Agent
errored" for every subagent; named subagents get the configured agent
name rendered to the right in secondary text (self-spawns and
unresolved names omit it — "Running self agent" is nonsense).
**ToolCallGroup** now detects `allSubagents` and swaps `StackedToolIcons`
for a single `Users` glyph — otherwise the group header shows a wrench
("tool") icon next to "Ran 5 agents", which reads wrong.
* 🪆 feat: delimiter-aware tool labels in ticker + full-width tool lines
New shared `parseToolName` helper in `client/src/utils/toolLabels.ts`
— single source of truth for splitting `<tool>_mcp_<server>` ids and
mapping native tool names (web_search, execute_code, …) to their
friendly translation keys. `ToolCallGroup` drops its inline copy and
pulls from this helper.
Ticker tool lines now use the shared parser + a new `ToolIdentifier`
sub-renderer so the live log reads like the main tool UI:
- MCP tool → `<server> · <code-badge:tool>` (e.g. "github · `search_code`")
- Native → friendly name from `TOOL_FRIENDLY_NAME_KEYS`
- Unknown → bare `<code>` badge of the raw id
The `using_tool` / `tool_complete` rows now render with a
`flex w-full items-baseline gap-1 overflow-hidden` layout matching
the writing/reasoning rows — they take the full container width
instead of collapsing to content size. Output snippets on
`tool_complete` get the same tail-side `dir="rtl"` ellipsis so the
newest characters stay flush-right when the container is narrow.
Dropped the now-unused template i18n keys
(`com_ui_subagent_ticker_using_with_args`,
`com_ui_subagent_ticker_tool_complete`,
`com_ui_subagent_ticker_tool_output`) in favor of tokens the JSX
composes structurally. Only English is touched per the project rule;
other locales follow externally.
* 🪆 fix: dialog scroll button + auto-scroll during streaming deltas
Two race/trigger bugs in the dialog's scroll behavior:
**Button never showed**: `addEventListener('scroll', …)` in a
`useEffect` ran before Radix's portal had actually committed the
scroll container, so `scrollRef.current` was still null — the listener
never attached, `isAtBottom` stayed stuck at its initial `true`, and
the jump-to-latest button was never rendered. Swap to React's
`onScroll` prop on the element itself so the handler wires up as part
of DOM commit, not a post-commit effect.
**Auto-scroll stalled during text streaming**: the pin-to-bottom
effect only re-fired on `contentParts.length` changes. Message/reasoning
deltas extend the last TEXT/THINK part's `.text` without changing the
array length — so the view would drift up as tokens piled in and
never catch back up. Replace the length-dep effect with a
`ResizeObserver` on the inner content div; every height change (new
part or in-place growth) triggers a scroll-pin when the user is still
at the bottom.
* 🪆 fix: drop leading ellipsis from ticker body
truncatePreview was prepending ... to the tail when the buffer
exceeded 300 chars. The component's CSS already produces a left-side
ellipsis for overflow via dir=rtl + text-overflow: ellipsis — stacking
a data-level ellipsis on top renders a stray dot character right after
the Writing: / Reasoning: label (Writing: .Sure!), which looks like a
typo to the reader.
Data now returns just the last 300 chars when truncating; CSS handles
the visual cue whenever the body actually overflows its container.
* 🪆 fix: Codex review — subagent isolation + concurrent-safe throttle
Three findings from the @codex review pass, all valid:
**P1 — buildAgentInput leaks parent discovered-tool state into subagent
children.** `buildAgentInput` mutates `agent.toolRegistry`
(`overrideDeferLoadingForDiscoveredTools` flips `defer_loading:true→false`
on tools the parent previously searched for) and appends those tools'
definitions to the returned `toolDefinitions` before the function
returns. `buildSubagentConfigs` was clearing the reported
`initialSummary` / `discoveredTools` fields on the returned
AgentInputs, but that happened post-return — the registry writes and
extra tool definitions persisted on the child, silently defeating
context isolation and inflating the child's prompt.
Fix: `buildAgentInput` now takes an `isSubagent` flag that gates the
registry-mutation block and omits `initialSummary` /
`discoveredTools` at the source. `buildSubagentConfigs` passes
`{ isSubagent: true }` for every explicit child; no post-hoc cleanup
needed.
**P2 — ToolCallGroup labels a finished subagent group as still
running when the child returned no output.** `getToolMeta` computed
`hasOutput` as `!!tc.output`, which is `false` for a completed
subagent that returned empty text (the UI already has an "empty
result" fallback for that case). `allCompleted` would stay `false`
and the group header stuck on "Running N agents" forever.
Fix: treat `tc.progress === 1` as completion too — progress is the
authoritative lifecycle signal, output is just content.
**P2 — useThrottledValue schedules `setTimeout` during render.**
Discarded renders under Strict Mode / Concurrent rendering would
leave orphan timers firing against stale trees.
Fix: move `setTimeout` into a `useEffect` keyed on `[value, intervalMs,
enabled]`. Render-time still mutates refs (idempotent), but timer
scheduling lives post-commit. Cleanup on unmount and on passthrough
transitions is preserved.
* 🪆 fix: Codex P2 — wipe subagent atoms on conversation switch
`clearStepMaps()` intentionally doesn't reset `subagentProgressByToolCallId`
so a user can reopen a completed subagent's dialog mid-conversation,
but `resetSubagentAtoms` was defined and never exposed / called —
so each completed run's aggregated `contentParts` + `tickerState`
stayed resident in the `atomFamily` for the whole app session.
Unbounded growth across multi-conversation sessions.
Expose `resetSubagentAtoms` from `useStepHandler` and fire it from
`useEventHandlers` whenever the URL's `conversationId` changes.
That's the correct cleanup boundary: historical subagent dialogs
rehydrate from persisted `subagent_content` on each `tool_call` at
message-save time, so wiping live atoms on switch doesn't lose any
viewable history — it just releases per-tool-call state that the
old conversation's components no longer subscribe to.
* 🪆 fix: Codex round 3 — subagent registry isolation + post-run label
Two more valid findings.
**P1 — parent-order registry mutation leaks into subagent inputs.**
`overrideDeferLoadingForDiscoveredTools` mutates `agent.toolRegistry`
in place (the Map *and* the LCTool objects inside it). When an agent
appears both as a handoff target (normal graph node) AND an explicit
subagent child, a subagent build that ran before the parent's build
captures a reference to the same registry — the parent's later mutation
leaks through to the child.
Fix: for subagent children (`isSubagent`), clone the `toolRegistry`
Map and shallow-clone each LCTool inside before returning the inputs.
`defer_loading` flips on parent-graph registry mutations can't
propagate across the clone boundary. `toolDefinitions` also gets a
shallow-copy pass so the same isolation holds for definitions the
child carries directly.
**P2 — "Running N agents" label stuck after cancel/error.**
ToolCallGroup's all-subagent label was gated only on `allCompleted`,
which requires every child to have `hasOutput || progress === 1`.
A subagent that gets cancelled (stream ends, no `stop` phase, no
output) never satisfies that — so even after `isSubmitting` flips
false, the header stays on "Running N agents" while each individual
card correctly shows "Cancelled agent".
Fix: derive a `subagentsDone` flag as `allCompleted || !isSubmitting`
and gate the past-tense label on that. Matches the tri-state each
SubagentCall card already resolves (finished / cancelled / running).
* 🪆 fix: Codex P2 — ACL-check subagents.agent_ids on create/update
Codex flagged that `subagents.agent_ids` was accepted as arbitrary
strings on the create/update routes while `edges` got a
`validateEdgeAgentAccess` pass — so users could save subagent
references to agents they can't VIEW. At runtime `initializeClient`'s
`processAgent` ACL gate silently drops those, so the persisted
configuration and the actual behavior diverged in a way that is
difficult to diagnose.
Refactor: extract the id-set → unauthorized-ids check into a shared
`collectUnauthorizedAgentIds`, wrap it with a dedicated
`validateSubagentAccess`, and plumb the same 403-on-failure response
the edge path already returns. Applied on both POST /agents and
PATCH /agents/:id.
* 🪆 fix: Codex round 5 — ACL-disable escape hatch + ticker order
Two valid findings.
**P1 — can't disable subagents after losing access to a child.**
The subagent ACL check ran on every create/update that echoed back
the `agent_ids` list, even when the user was explicitly disabling
the feature. The UI keeps the list intact when toggling `enabled:
false`, so a user who subsequently lost VIEW on any child would be
locked in a 403 loop — every edit (including the one that turns
subagents off) bounces.
Fix: gate the ACL check on `subagents.enabled !== false` at both
the POST /agents and PATCH /agents/:id handlers. Empty list stays
a no-op. Disabling the feature is always permitted.
**P2 — ticker fold merges out-of-order previews across delta
switches.** `foldSubagentEventIntoTicker` carried `textLineIdx` /
`thinkLineIdx` across a reasoning → text → reasoning transition,
so the second reasoning chunk appended to the original reasoning
line instead of starting a new chronological one.
Fix: close the opposite buffer + cursor when a delta-type switch
is detected (same rule the content-parts reducer already applies).
Added a regression test.
* 🪆 fix: Codex round 6 — preserve mid-stream atoms + honor sequential suppression
Two valid findings.
**P2 — atom reset fires on initial chat URL assignment.**
`useEventHandlers` initialized `lastConversationIdRef` from the URL's
current `paramId`, then reset subagent atoms whenever the ref and
`paramId` disagreed. For a brand-new conversation the URL stamp goes
from `undefined → "abc123"` while the first response is still
streaming, which used to drop subagent ticker/content state mid-run
and leave dialogs missing earlier updates.
Fix: only reset when *both* the old and new IDs are non-null and
differ — i.e. a user-initiated switch between two established
conversations. The initial assignment passes through without
clearing.
**P2 — ON_SUBAGENT_UPDATE bypassed `hide_sequential_outputs`.**
Every other streaming handler in `callbacks.js`
(`ON_RUN_STEP`, `ON_MESSAGE_DELTA`, etc.) gates emission on
`checkIfLastAgent` + `metadata?.hide_sequential_outputs`, but the
subagent forwarder did an unconditional `emitEvent` — so intermediate
agents in a sequential chain were leaking their children's activity
to the client even when the chain was configured to suppress
intermediates.
Fix: accept `metadata` and apply the same `isLastAgent ||
!hide_sequential_outputs` gate. Aggregation still runs regardless of
visibility (persistence + dialog depend on it); only the SSE forward
is suppressed.
* 🪆 fix: Codex P2 — gate subagent ACL check on endpoint capability
`validateSubagentAccess` ran on every create/update where
`subagents.enabled !== false`, regardless of the endpoint-level
`subagents` capability. When the capability is off at the appConfig
level, `initializeClient` already strips the `subagents` block at
runtime — so persisted `agent_ids` are inert — but the validation
could still 403 on a legacy record whose referenced child is no
longer viewable, blocking unrelated edits.
Fix: add `isSubagentsCapabilityEnabled(req)` that reads the agents
endpoint's capabilities from `req.config` and gate both the create
and update ACL checks on it. Capability-off environments can update
agents with stale `subagents` data freely; capability-on keeps the
full ACL protection.
* 🪆 fix: Codex P2 — reset subagent atoms on id→null navigation too
Previous guard (both-established) skipped the reset whenever
`paramId` became null/undefined, so navigating from an existing
chat to a "new chat" route left stale subagent progress resident
in the `atomFamily` until the user picked a specific different
chat.
Swap the both-established check for a one-time flag: skip only the
very first `undefined → id` transition (the brand-new-chat URL
stamp that happens mid-stream), then reset on any subsequent
change — id→id, id→null, null→id-after-reset. If the user started
on an established chat the flag is true at mount, so the guard is
a no-op and every navigation resets normally.
* 🪆 fix: Codex round 9 — subagent persistence gate + handoff children
Two valid findings.
**P1 — hide_sequential_outputs also gates persistence.**
The previous fix gated the SSE forward on
`isLastAgent || !hide_sequential_outputs` but still ran the
per-tool-call `createContentAggregator` aggregation unconditionally.
`finalizeSubagentContent` would then attach the hidden intermediate
agent's child reasoning / tool output to the saved message, so a
page refresh could reveal activity that was intentionally suppressed
live. Move the visibility gate to the top of the handler — hidden
agents now skip both aggregation and emission, so
"hide_sequential_outputs" is a consistent "don't record" rule for
subagent traces.
**P2 — handoff agents' explicit subagents were silently dropped.**
`initializeClient` only resolved `subagentAgentConfigs` for the
primary config, so an agent used via handoff that had its own
`subagents.agent_ids` saved in the builder would get self-spawn
only; every explicit child was quietly ignored, creating a
saved-config / runtime mismatch the user couldn't diagnose.
Extract the resolution into a shared `loadSubagentsFor(config)`
helper and invoke it for the primary and every handoff agent in
`agentConfigs`. The `edgeAgentIds` precomputation stays outside
the helper (it's loop-invariant). Capability-off shortcuts return
empty early so the existing strip-on-capability-off path still
holds.
* 🪆 fix: Codex P2 — recursive subagent build for multi-level delegation
Previously only the outer `agents[]` loop attached `subagentConfigs`
to its inputs, so a child used as a subagent (invoked via the
`subagent` tool) lost every explicit spawn target of its own. A
user-valid configuration like A → B → C would only run the top
layer; B could never actually delegate to C from inside A's run.
Recursively build `subagentConfigs` for each child inside
`buildSubagentConfigs`, passing the child's freshly-constructed
`childInputs` down so its own `subagents.enabled` children get
resolved too. Added cycle protection via an `ancestors` Set — a
configuration like A → B → A is safely cut off at the second
encounter of A rather than recursing forever (the existing
`child.id === agent.id` guard already prevents the direct self-loop).
* 🪆 fix: Codex P2 — reset subagent atoms on useEventHandlers unmount
The effect that resets subagent atoms only fired on `paramId` change,
so unmounting the chat container (route change away from /c) never
flushed the atoms. `knownSubagentAtomKeys` lives in a ref inside
`useStepHandler` — once the hook unmounts the ref is gone, so a
subsequent remount can't clean atoms it never registered.
Added a second `useEffect` that only runs cleanup on unmount (empty
deps aside from the stable `resetSubagentAtoms` callback). Keeps
`atomFamily` bounded across full route teardowns too.
* 🪆 fix: Codex round 13 — cyclic subagent guard + prefer persisted
Two valid findings.
**P1 — cyclic subagent ref reloads the primary.** A configuration
like `A ↔ B` (B lists A as its own subagent) would send
`loadSubagentsFor` down a path that couldn't find A in
`agentConfigs` (the primary isn't stored there), so it called
`processAgent(A)` a second time. That inserts a fresh config for
the primary id, which downstream duplicates in
`[primary, ...agentConfigs.values()]` and can replace the primary's
tool context with the reloaded copy.
Fix: short-circuit when a subagent ref points back at
`primaryConfig.id` — reuse the already-loaded primary config.
Primary is always an edge id so no pruning bookkeeping needed.
**P2 — live atom preferred over canonical persisted trace.** The
dialog picked `progress.contentParts` ahead of
`persistedContent`, but the Recoil bucket is best-effort — after
a disconnect/reconnect it can be stale or partial. The server's
`subagent_content` on the `tool_call` is the canonical record
refreshed on sync. Preferring live could hide completed
tool/reasoning history that was actually persisted.
Fix: flip the preference order. Persisted wins when it's
non-empty; live covers the mid-stream window (before the parent
message saves, persisted is empty) and the older-runs fallback.
Updated the test that enforced the old order to lock the new
semantics in (separate mid-stream live-fallback assertion kept).
* 🪆 fix: Codex P2 — subagent atom reset rule simplified to 'leaving established id'
The `hasEstablishedConversationRef` + check for initial undefined→id
covered the first navigation but missed the equivalent mid-stream
URL stamp when a user goes from an existing chat to a new chat and
sends a message there (`id → null → newId`). The null → newId
transition was still hitting the reset branch and wiping the
in-flight subagent ticker/content for that first turn.
Simpler rule: only reset when the PREVIOUS paramId is an established
id. Every transition AWAY from an established chat clears (id→id2,
id→null, id→undefined); every transition FROM null/undefined passes
through (initial mount, new-chat URL stamp mid-stream). Drop the
`hasEstablishedConversationRef` machinery in favor of that single
condition.
* 🪆 fix: Codex P2 — match runtime's strict subagent enable check in ACL
Runtime (`initializeClient` + `run.ts`) treats `subagents?.enabled`
as a truthy predicate — `undefined`, `null`, missing, and `false`
all short-circuit. The ACL gate was using `!== false` which
accepted `undefined` / missing as "enabled" and could 403 a payload
whose subagent tool would be inert at runtime.
Swap both create and update to `enabled === true`. Only a strictly-
enabled payload triggers the ACL check; the disable path (`false`)
still passes through so a user who lost VIEW on a child can still
save the disable edit.
* 🪆 fix: Codex P2 — reject missing subagent references with 400
`validateSubagentAccess` collapsed through `collectUnauthorizedAgentIds`,
which returns an empty list for ids with no DB record — so typos and
references to deleted agents passed validation silently, and
`initializeClient` later dropped them at runtime. Saved config would
then list spawn targets that the backend never honored, a hard-to-
diagnose drift.
Refactor the helper into `classifyAgentReferences(ids, …)` which
returns `{ missing, unauthorized }` separately. `validateEdgeAgentAccess`
keeps its old semantics (missing is intentional — a self-referential
`from` names the agent being created). `validateSubagentReferences`
surfaces both buckets so the create/update handlers can 400 on
missing and 403 on unauthorized with distinct error messages and
`agent_ids` lists.
* 🪆 polish: tighten subagent dialog grid gap to gap-2
OGDialogContent's grid default is `gap-4`, which renders the title,
description, and scroll area as three visually separated panels.
Drop to `gap-2` so they read as one block.
* 🪆 polish: swap Subagents above Handoffs in Advanced panel
Subagents is the more common knob users reach for, so show it first.
Handoffs keep the same Controller wiring, just move below.
* 🛠️ feat: Add registerCodeExecutionTools helper
Idempotently registers `bash_tool` + `read_file` in the run's tool
registry and tool-definition list via a registry `.has()` dedupe. Sets
up the single code-execution tool path shared by:
- `initializeAgent` (when an agent has `execute_code` in its tools and
the capability is enabled for the run)
- `injectSkillCatalog` (when skills are active; unconditional read_file,
bash_tool follows `codeEnvAvailable`)
Both callers reach the helper in the same initialization sequence, so
the second call becomes a no-op and exactly one copy of each tool
reaches the LLM — no more double registration for agents that combine
`execute_code` capability with active skills.
Unit-tested on a fresh run, idempotence (second call, overlap with
prior tooldefs, partial overlap), and the no-registry variant.
* 🔀 refactor: Route injectSkillCatalog bash_tool + read_file through registerCodeExecutionTools
The `skill` tool is still registered inline (it's skill-path-specific),
but `bash_tool` + `read_file` now flow through the shared idempotent
helper so a prior registration from the execute_code path doesn't
produce a duplicate copy later in the same run. Behavior preserved:
- `read_file` always registers when any active skill is in scope —
manually-primed `disable-model-invocation: true` skills still need it
to load `references/*` from storage.
- `bash_tool` follows `codeEnvAvailable` exactly as before.
Adds a test pinning the cross-call dedupe: when `injectSkillCatalog`
runs AFTER `registerCodeExecutionTools` has already seeded the registry
+ tool definitions with bash_tool/read_file, the resulting
`toolDefinitions` still contains exactly one copy of each.
* 🪄 feat: Expand `execute_code` tool name into bash_tool + read_file at initialize-time
When an agent's `tools` include `execute_code` and the `execute_code`
capability is enabled for the run, `initializeAgent` now registers
`bash_tool` + `read_file` via `registerCodeExecutionTools` before
`injectSkillCatalog`. The legacy `execute_code` tool definition is no
longer handed to the LLM — `execute_code` remains on the agent
document as a capability-trigger marker, but the runtime expands it
into the skill-flavored tool pair.
Call ordering matters: the `execute_code` registration runs BEFORE
`injectSkillCatalog`, so the skill path's own `registerCodeExecutionTools`
call inside `injectSkillCatalog` becomes a no-op via the registry's
`.has()` check. Exactly one copy of each tool reaches the LLM whether
the agent has:
- only `execute_code` (legacy path)
- only skills
- both
No data migration needed — `agent.tools: ['execute_code']` stays in
the DB unchanged; the expansion is a runtime operation.
Three tests cover the matrix: execute_code + capability on →
bash_tool + read_file registered; execute_code + capability off →
neither registered; no execute_code + capability on → neither
registered.
* 🗑️ refactor: Drop CodeExecutionToolDefinition from the builtin registry
Removes the legacy `execute_code` entry from `agentToolDefinitions` and
the corresponding import. With the initialize-time expansion in place,
nothing consults `getToolDefinition('execute_code')` for a tool schema
any more — the capability gate still filters on the string
`execute_code`, but the actual tool definitions the LLM sees come from
`registerCodeExecutionTools` (i.e. `bash_tool` + `read_file`).
`loadToolDefinitions` in `packages/api/src/tools/definitions.ts`
silently drops `execute_code` when it no longer resolves in the
registry — that's the expected path and is now covered by an updated
test. No caller of `getToolDefinition('execute_code')` expects a
non-undefined result after this change.
* 🔌 refactor: Read CODE_API_KEY from env for primeCodeFiles + PTC
Finishes the Phase 4 server-env-keyed rollout on the two remaining
`loadAuthValues({ authFields: [EnvVar.CODE_API_KEY] })` sites in
`ToolService.js`:
- `primeCodeFiles` (user-attached file priming on execute_code agents)
- Programmatic Tool Calling (`createProgrammaticToolCallingTool`)
Both now read `process.env[EnvVar.CODE_API_KEY]` directly, matching
`bash_tool`'s pattern. The per-user plugin-auth path is no longer
consulted for code-env credentials anywhere in the hot path — the
agents library owns the actual tool-call execution and also reads the
env var internally.
Priming still fires for existing user-file workflows so the legacy
`toolContextMap[execute_code]` hint ("files available at /mnt/data/...")
stays in the prompt; only the key lookup changed.
* 🔧 fix: Type the pre-seeded dedupe-test tools as LCTool
CI TypeScript type checks caught `{ parameters: {} }` in the new
cross-call dedupe test: `LCTool.parameters` is a `JsonSchemaType`,
not `{}`. Use `{ type: 'object', properties: {} }` and type the
local registry Map through the parameter-derived shape so the
pre-seeded values match what `toolRegistry.set` expects.
* 🛡️ fix: Run execute_code expansion before GOOGLE_TOOL_CONFLICT gate
Codex review caught a latent regression: the original Phase 8 placement
ran `registerCodeExecutionTools` after `hasAgentTools` was computed,
so an execute-code-only agent on Google/Vertex with provider-specific
`options.tools` populated would no longer trip `GOOGLE_TOOL_CONFLICT`
— the legacy `CodeExecutionToolDefinition` used to populate
`toolDefinitions` before the guard, but after dropping it from the
registry, `toolDefinitions` stayed empty until my expansion ran
downstream of the guard. Mixed provider + agent tools would silently
flow through to the LLM.
Fix moves the `execute_code` expansion to BEFORE `hasAgentTools`
computation. `bash_tool` + `read_file` now contribute to the check
the same way the legacy `execute_code` def did. Covered by a new
test that pins the Google+execute_code+provider-tools scenario —
the `rejects.toThrow(/google_tool_conflict/)` path would have
silently passed on the prior placement.
* 🔗 fix: Thread codeEnvAvailable through handoff sub-agents
Round-2 codex review caught the other half of the execute_code
expansion gap: `discoverConnectedAgents` omitted `codeEnvAvailable`
from its forwarded `initializeAgent` params, so handoff sub-agents
with `agent.tools: ['execute_code']` lost the `bash_tool` + `read_file`
registration (pre-Phase 8 the legacy `CodeExecutionToolDefinition`
would have landed in their `toolDefinitions` via the registry).
- Add `codeEnvAvailable?` to `DiscoverConnectedAgentsParams` and
forward it verbatim on every sub-agent `initializeAgent` call.
- Update the three JS call sites that construct the primary's
`codeEnvAvailable` (`services/Endpoints/agents/initialize.js`,
`controllers/agents/openai.js`, `controllers/agents/responses.js`)
to pass the same flag into `discoverConnectedAgents` — one
authoritative source per request.
- Two regression tests in `discovery.spec.ts` pin the true/false
passthrough so a future refactor that drops the param-forwarding
surfaces immediately.
Left intentionally unchanged: `packages/api/src/agents/openai/service.ts`
(public API helper with no in-repo caller). External consumers of
`createAgentChatCompletion` who want code execution should pass a
`codeEnvAvailable`-aware `initializeAgent` via `deps` — documenting
the full public-API surface is out of scope for this Phase 8 PR.
* 🔗 fix: Thread codeEnvAvailable through addedConvo + memory-agent paths
Round-3 codex review caught the last two production `initializeAgent`
callers missing the Phase-8 capability flag:
- `api/server/services/Endpoints/agents/addedConvo.js` (multi-convo
parallel agent execution). Added `codeEnvAvailable` to
`processAddedConvo`'s destructured params and forwarded it into
the per-added-agent `initializeAgent` call. Caller in
`api/server/services/Endpoints/agents/initialize.js` passes the
same `codeEnvAvailable` it computed for the primary.
- `api/server/controllers/agents/client.js` (`useMemory` — memory
extraction agent). Computes its own `codeEnvAvailable` from
`appConfig?.endpoints?.[EModelEndpoint.agents]?.capabilities` and
forwards into `initializeAgent`. Memory agents rarely list
`execute_code`, but if one does, pre-Phase 8 they got the legacy
`execute_code` tool registered unconditionally — the passthrough
restores parity.
With this, every production caller of `initializeAgent` explicitly
resolves the capability: main chat flow (primary + handoff), OpenAI
chat completions (primary + handoff), Responses API (primary + handoff),
added convo parallel agents, and memory agents. The one remaining
caller, `packages/api/src/agents/openai/service.ts::createAgentChatCompletion`,
is a public API helper with no in-repo consumer (external callers
must pass a capability-aware `initializeAgent` via `deps`).
* 🪤 fix: Remove duplicate appConfig declaration causing TDZ ReferenceError
The Responses API controller had TWO `const appConfig = req.config;`
bindings inside `createResponse`: one at the top of the function
(added by the Phase 4 `bash_tool` decouple) and one inside the try
block (added by the polish PR #12760). Because `const` is block-scoped
with a temporal dead zone, the inner redeclaration put `appConfig` in
TDZ for the entire try block, so any earlier reference inside the
try — notably `appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders`
at line 348 — threw `ReferenceError: Cannot access 'appConfig'
before initialization`. The error was silently swallowed by the
outer try/catch, leaving `recordCollectedUsage` unreached and the
six `responses.unit.spec.js` token-usage tests failing.
Removing the inner redeclaration fixes the six failing tests
(verified: 11/11 pass locally post-fix, 0 regressions elsewhere).
The outer function-scoped binding already provides `appConfig` to
every downstream reference.
* 🔗 fix: Thread codeEnvAvailable through the OpenAI chat-completion public API
Round-4 codex review (legitimate on the type-safety angle, even though the
runtime concern was already covered): the `createAgentChatCompletion`
helper defines its own narrower `InitializeAgentParams` interface locally,
and the type was missing `codeEnvAvailable`. External consumers who
supply a capability-aware `deps.initializeAgent` couldn't route
`codeEnvAvailable` through without a type-cast workaround.
- Widen the local `InitializeAgentParams` interface to include
`codeEnvAvailable?: boolean` (matches the real
`packages/api/src/agents/initialize.ts` type).
- Derive `codeEnvAvailable` inside `createAgentChatCompletion` from
`deps.appConfig?.endpoints?.agents?.capabilities` (the same source
the in-repo controllers use) and forward to `deps.initializeAgent`.
Uses a string literal `'execute_code'` lookup so this file stays free
of a `librechat-data-provider` import — keeping the dependency surface
of the public helper minimal.
With this, external consumers of `createAgentChatCompletion` who pass
`appConfig` with the agents capabilities get `bash_tool` + `read_file`
registration automatically; consumers who don't pass `appConfig` retain
the existing "explicit opt-in" semantics (the flag stays `undefined`,
expansion is skipped).
* 🧹 chore: Review-driven polish — observability log, JSDoc DRY, test gaps, no-op allocation
Addresses the comprehensive review of PR #12767:
- **Finding #1** (MINOR, observability): `initializeAgent` now emits a
debug log when an agent lists `execute_code` in its tools but the
runtime gate is off (`params.codeEnvAvailable` !== true). The
event-driven `loadToolDefinitionsWrapper` path doesn't log
capability-disabled warnings, so without this the tool silently
vanishes from the LLM's definitions with zero trace. Operators
debugging "why isn't code interpreter working?" now get a signal at
the initialize layer.
- **Finding #5** (NIT, allocation): `registerCodeExecutionTools` now
returns the input `toolDefinitions` array by reference on the no-op
path (both tools already registered by a prior caller in the same
run) instead of allocating a fresh spread array every time. The
common dual-call scenario — `initializeAgent` then
`injectSkillCatalog` — saves one O(n) copy per request.
- **Finding #4** (NIT, DRY): Collapsed the duplicated 6-line JSDoc
comment in `openai.js`, `responses.js`, and `addedConvo.js` into
either a one-line `@see DiscoverConnectedAgentsParams.codeEnvAvailable`
pointer (the two JS call sites) or a compact 3-line block referring
back to the canonical source (addedConvo's @param).
- **Finding #2** (MINOR, test gap): Added
`api/server/services/Endpoints/agents/addedConvo.spec.js` with three
cases covering `codeEnvAvailable=true`, `codeEnvAvailable=false`,
and omitted (undefined) passthrough. A future refactor that drops
the param from destructuring now surfaces here instead of silently
regressing multi-convo parallel agents with `execute_code`.
- **Finding #3** (MINOR, test gap): Added
`api/server/controllers/agents/__tests__/client.memory.spec.js`
pinning the capability-flag derivation that `AgentClient::useMemory`
uses — six cases covering present/absent/null/undefined config shapes
plus an enum-literal pin (`'execute_code'` / `'agents'`). Catches
enum renames or config-path shifts that would otherwise silently
strip `bash_tool` + `read_file` from memory agents.
Finding #7 (jest.mock scoping, confidence 40) left as-is: the
reviewer's own risk assessment noted `buildToolSet` doesn't touch
the mocked exports, and restructuring a file-level `jest.mock` to
`jest.doMock` + dynamic `import()` introduces more complexity than
the speculative risk justifies. The existing mock is scoped to the
test file and contains the same stubs the adjacent
`skills.test.ts` already uses.
Finding #6 (PR description commit count) addressed out-of-band via
PR description update.
All existing tests pass, typecheck clean, lint clean across touched
files. New tests: 9 cases across 2 new spec files.
* 🧽 refactor: Replace hardcoded 'execute_code' string with AgentCapabilities enum in service.ts
Follow-up review (conf 55) caught that `openai/service.ts`'s Phase 8
`codeEnvAvailable` derivation used the literal `'execute_code'` while
every in-repo controller uses `AgentCapabilities.execute_code` from
`librechat-data-provider`. The file deliberately uses local type
interfaces to keep the public API helper's type surface small, but
that pattern was never a ban on single-value imports from the data
provider — `packages/api` already depends on it. Importing the enum
value means a future rename of `AgentCapabilities.execute_code`
propagates to this file automatically, matching the in-repo
controllers' behavior.
Other follow-up findings left as-is per the reviewer's own verdict:
- #2 (memory spec mirrors the production expression rather than
calling `AgentClient::useMemory` directly): reviewer flagged as
"not blocking" / "design-philosophy observation." The test file's
JSDoc already explicitly documents the tradeoff and pins the enum
literals to catch the most likely drift vector. Standing up
`AgentClient` + all its mocks for a one-line regression guard is
disproportionate.
- #3 (`addedConvo.spec.js` mock signature vs. underlying
`loadAddedAgent` arity): reviewer's own confidence 25 noted the
mock matches the wrapper's actual call pattern in the production
file. Not a real gap.
- #4 was self-retracted as a false alarm.
* 🗑️ refactor: Fully deprecate CODE_API_KEY — remove all LibreChat-side references
The code-execution sandbox no longer authenticates via a per-run
`CODE_API_KEY` (frontend or backend). Auth moved server-side into the
agents library / sandbox service, so LibreChat drops every reference:
**Backend plumbing:**
- `api/server/services/Files/Code/crud.js`: `getCodeOutputDownloadStream`,
`uploadCodeEnvFile`, `batchUploadCodeEnvFiles` no longer accept
`apiKey` or send the `X-API-Key` header.
- `api/server/services/Files/Code/process.js`: `processCodeOutput`,
`getSessionInfo`, `primeFiles` drop the `apiKey` param throughout.
- `api/server/services/ToolService.js`: stop reading
`process.env[EnvVar.CODE_API_KEY]` for `primeCodeFiles` and PTC; the
agents library handles auth internally. Remove the now-dead
`loadAuthValues` + `EnvVar` imports. Drop the misleading
"LIBRECHAT_CODE_API_KEY" hint from the bash_tool error log.
- `api/server/services/Files/process.js`: remove the `loadAuthValues`
call around `uploadCodeEnvFile`.
- `api/server/routes/files/files.js`: code-env file download no longer
fetches a per-user key.
- `api/server/controllers/tools.js`: `execute_code` is no longer a
tool that needs verifyToolAuth with `[EnvVar.CODE_API_KEY]` — the
endpoint always reports system-authenticated so the client skips
the key-entry dialog. `processCodeOutput` called without `apiKey`.
- `api/server/controllers/agents/callbacks.js`: `processCodeOutput`
invoked without the loadAuthValues round trip, for both LegacyHandler
and Responses-API handlers.
- `api/app/clients/tools/util/handleTools.js`: `createCodeExecutionTool`
called with just `user_id` + files.
**packages/api:**
- `packages/api/src/agents/skillFiles.ts`: `PrimeSkillFilesParams`,
`PrimeInvokedSkillsDeps`, `primeSkillFiles`, `primeInvokedSkills` all
drop the `apiKey` param; the gate is purely `codeEnvAvailable`.
- `packages/api/src/agents/handlers.ts`: `handleSkillToolCall` drops
the `process.env[EnvVar.CODE_API_KEY]` read; skill-file priming is
now gated solely on `codeEnvAvailable`. `ToolExecuteOptions`
signatures drop apiKey from `batchUploadCodeEnvFiles` and
`getSessionInfo`.
- `packages/api/src/agents/skillConfigurable.ts`: JSDoc no longer
references the env var.
- `packages/api/src/tools/classification.ts`: PTC creation no longer
gated on `loadAuthValues`; `buildToolClassification` drops the
`loadAuthValues` dep entirely (no LibreChat-side callers need it for
this path anymore).
- `packages/api/src/tools/definitions.ts`: `LoadToolDefinitionsDeps`
drops the `loadAuthValues` field.
**Frontend:**
- Delete `client/src/hooks/Plugins/useAuthCodeTool.ts`,
`useCodeApiKeyForm.ts`, and
`client/src/components/SidePanel/Agents/Code/ApiKeyDialog.tsx` —
the install/revoke dialogs for CODE_API_KEY are fully dead.
- `BadgeRowContext.tsx`: drop `codeApiKeyForm` from the context type and
provider. `codeInterpreter` toggle treated as always authenticated
(sandbox auth is server-side).
- `ToolsDropdown.tsx`, `ToolDialogs.tsx`, `CodeInterpreter.tsx`,
`RunCode.tsx`, `SidePanel/Agents/Code/Action.tsx` +`Form.tsx`: all
API-key dialog trigger refs, "Configure code interpreter" gear
buttons, and auth-verification plumbing removed. The
"Code Interpreter" toggle is now a plain `AgentCapabilities.execute_code`
checkbox — no key-entry gate.
- `client/src/locales/en/translation.json`: drop the three
`com_ui_librechat_code_api*` keys and `com_ui_add_code_interpreter_api_key`.
Other locales are externally automated per CLAUDE.md.
**Config:**
- `.env.example`: remove the `# LIBRECHAT_CODE_API_KEY=your-key` section
and its header.
**Tests:**
- `crud.spec.js`: assertions flipped to pin "no X-API-Key header" and
"no apiKey param".
- `skillFiles.spec.ts`: removed env-var save/restore; tests now pin
that the batch-upload path is gated solely on `codeEnvAvailable` and
that no apiKey is threaded through.
- `handlers.spec.ts`: same — just the `codeEnvAvailable` gate pins
remain.
- `classification.spec.ts`: remove the two tests that asserted
`loadAuthValues` was (not) called for PTC.
- `definitions.spec.ts`: drop every `loadAuthValues: mockLoadAuthValues`
entry from the deps shape.
- `process.spec.js`: strip the mock of `EnvVar.CODE_API_KEY`.
**Comment hygiene:**
- `tools.ts`, `initialize.ts`, `registry/definitions.ts`: shortened
stale comment references to "legacy `execute_code` tool" without
naming the retired env var.
Tests verified: 678 packages/api tests pass, 836 backend api tests
pass. Typecheck clean, lint clean. Only remaining CODE_API_KEY
mentions in the code are two regression-guard assertions:
- `crud.spec.js`: pins "no X-API-Key header" stays absent.
- `skillConfigurable.spec.ts`: pins `configurable` never grows a
`codeApiKey` field.
* 🧹 chore: Remove the last two CODE_API_KEY name mentions in LibreChat
Follow-up to the prior full deprecation commit: two tests still named
the retired identifier in their regression-guard assertions.
- `packages/api/src/agents/skillConfigurable.spec.ts`: drop the
"does not inject a codeApiKey key" test. The `codeApiKey` field is
gone from the production configurable shape, so an absence-assertion
naming it re-introduces the retired identifier in code.
- `api/server/services/Files/Code/crud.spec.js`: rename the
"without an X-API-Key header" case back to "should request stream
response from the correct URL" and drop the
`expect(headers).not.toHaveProperty('X-API-Key')` assertion. The
surrounding request-shape checks (URL, timeout, responseType) still
pin the behavior; the explicit header-absence line was named-after
the deprecated contract.
Result: `grep -rn "CODE_API_KEY\|codeApiKey\|LIBRECHAT_CODE_API_KEY"`
against the LibreChat source tree returns zero hits. The only
remaining `X-API-Key` strings in this repo are on unrelated OpenAPI
Action + MCP server auth configurations, where the string is
user-facing config, not a LibreChat-owned identifier.
Tests: 677 packages/api pass (2 pre-existing summarization e2e failures
unrelated); 126 api-workspace controller/service tests pass.
Typecheck and lint clean.
* 🎯 fix: Narrow codeEnvAvailable to per-agent (admin cap AND agent.tools)
Before this commit, `codeEnvAvailable` was computed in the three JS
controllers as the admin-level capability flag only
(`enabledCapabilities.has(AgentCapabilities.execute_code)`) and passed
through `initializeAgent` → `injectSkillCatalog` / `primeInvokedSkills` /
`enrichWithSkillConfigurable` unchanged. A skills-only agent whose
`tools` array didn't include `execute_code` still got `bash_tool`
registered (via `injectSkillCatalog`) and skill files re-primed to the
sandbox on every turn — wrong, because the agent never opted in to
code execution.
**Fix:** `initializeAgent` now computes the per-agent effective value
once as `params.codeEnvAvailable === true && agent.tools.includes(Tools.execute_code)`,
reuses the same boolean for:
1. The `execute_code` → `bash_tool + read_file` expansion gate
(previously already consulted `agent.tools`; now shares the single
`effectiveCodeEnvAvailable` binding).
2. The `injectSkillCatalog` call (previously got the raw admin flag).
3. The returned `InitializedAgent.codeEnvAvailable` field (new, typed as
required boolean).
**Controllers (initialize.js, openai.js, responses.js):** store
`primaryConfig.codeEnvAvailable` in `agentToolContexts.set(primaryId, ...)`,
capture `config.codeEnvAvailable` in every handoff `onAgentInitialized`
callback, and read it from the per-agent ctx inside the
`toolExecuteOptions.loadTools` runtime closure. The hoisted
`const codeEnvAvailable = enabledCapabilities.has(...)` locals in the
two OpenAI-compat controllers are gone — they were shadowing the
narrowed per-agent value.
**primeInvokedSkills:** `handlePrimeInvokedSkills` in
`services/Endpoints/agents/initialize.js` now uses
`primaryConfig.codeEnvAvailable` (per-agent, narrowed) instead of the
raw admin flag. A skills-only primary agent won't re-prime historical
skill files to the sandbox even when the admin enabled the capability
globally.
**Efficiency:** one extra `&&` in `initializeAgent`. No runtime hot-path
cost — the `includes()` scan on `agent.tools` was already happening for
the `execute_code` expansion gate; it's now just bound to a local. Tool
execution closures read `ctx.codeEnvAvailable === true` (property
access + strict equality, O(1)).
**Ephemeral-agent note:** per-agent narrowing is authoritative for both
persisted and ephemeral flows. The ephemeral toggle
(`ephemeralAgent.execute_code`) is reconciled into `agent.tools`
upstream in `packages/api/src/agents/added.ts`, so
`agent.tools.includes('execute_code')` is the single source of truth
by the time `initializeAgent` runs.
**Tests:** two new regression tests pin the narrowing contract:
- `initialize.test.ts` — four-quadrant matrix on
`InitializedAgent.codeEnvAvailable` (cap on × agent asks, cap on ×
doesn't ask, cap off × asks, neither). Catches future refactors that
drop either half of the AND.
- `skills.test.ts` — `injectSkillCatalog` with `codeEnvAvailable: false`
against an active skill catalog must NOT register `bash_tool` even
though it still registers `read_file` + `skill`. This is the state
a skills-only agent gets post-narrowing.
All 191 affected packages/api tests pass + 836 backend api tests pass.
Typecheck clean, lint clean.
* 🧽 refactor: Comprehensive-review polish — hoist tool defs, pin verifyToolAuth contract, doc appConfig
Addresses the comprehensive review of Phase 8. Findings mapped:
**#1 (MINOR): `verifyToolAuth` unconditional auth for execute_code**
- Added doc comment explicitly stating the deployment contract
(admin capability → reachable sandbox; no per-check health probe
to keep UI-gate queries O(1)).
- New `api/server/controllers/__tests__/tools.verifyToolAuth.spec.js`
with 4 regression tests pinning the contract:
1. `authenticated: true` + `SYSTEM_DEFINED` for execute_code.
2. 404 for unknown tool IDs.
3. `loadAuthValues` is never consulted (catches a future revert
that would resurface the per-user key-entry dialog).
4. Response `message` is never `USER_PROVIDED`.
**#2 (MINOR): `openai/service.ts` undocumented `appConfig` dependency**
- Expanded the `ChatCompletionDependencies.appConfig` JSDoc to spell
out that omitting it silently disables code execution for agents
with `execute_code` in their tools. External consumers of
`createAgentChatCompletion` now have the contract documented at
the type boundary.
**#5 (NIT): `registerCodeExecutionTools` re-allocates tool defs**
- Hoisted `READ_FILE_DEF` and `BASH_TOOL_DEF` to module-level
`Object.freeze`d constants. The shapes derive entirely from
static `@librechat/agents` exports, so a single frozen object per
tool is safe to share across every agent init. Eliminates the
~4-property allocations on every call (including the common
second-call no-op path).
**#6 (NIT): Verbose history-priming comment in initialize.js**
- Trimmed the 16-line `handlePrimeInvokedSkills` block to a 5-line
summary with `@see InitializedAgent.codeEnvAvailable` pointer.
The canonical narrowing explanation lives on the type; the
controller comment is just the ACL-vs-capability rationale.
**Skipped:**
- #3 (memory spec tests a mirror function): reviewer self-dismissed
as a design tradeoff; the enum-literal pin already catches the
highest-risk drift vector.
- #4 (cross-repo contract for `createCodeExecutionTool`): user will
explicitly install the latest `@librechat/agents` dev version
once the companion PR publishes, so the version pin will be
authoritative.
- #7 (migration/deprecation note for self-hosters): out of scope
per user direction — release notes handle this.
Tests verified: 679 packages/api + 840 backend api tests pass.
Typecheck + lint clean.
* 🔧 chore: Update @librechat/agents version to 3.1.68-dev.1 across package-lock and package.json files
This commit updates the version of the `@librechat/agents` package from `3.1.68-dev.0` to `3.1.68-dev.1` in the `package-lock.json` and relevant `package.json` files. This change ensures consistency across the project and incorporates any updates or fixes from the new version.
* 🔐 chore: Skills permissions housekeeping — reachable admin dialog + defaults tests
Phase 9 housekeeping pass. Skills was already gated on `PermissionTypes.SKILLS`
(seeded from `interface.skills`) and `AgentCapabilities.skills` everywhere it
matters, but two smaller parity gaps with Prompts/Memory/MCP remained:
- The skills admin settings dialog had no UI entry point. The only mount was
inside an unused `FilterSkills` component, so admins had no way to reach the
role-permissions editor for skills. Mounted it in `SkillsAccordion` gated on
`SystemRoles.ADMIN`, matching the `PromptsAccordion` pattern.
- No regression lock on skill permission defaults. `roles.spec.ts` asserted
structural completeness but not the specific shape — a future refactor
could silently flip ADMIN's `USE/CREATE/SHARE/SHARE_PUBLIC` to false or
drop SKILLS from USER defaults without failing. Added explicit Skills
assertions for both roles.
- No lock on `AgentCapabilities.skills` being in `defaultAgentCapabilities`.
Added an assertion in `endpoints.spec.ts`.
* 🩹 fix: Remove duplicate `const appConfig` in Responses createResponse
The Skills polish commit (#12760) added `const appConfig = req.config;` at
line 381 inside the try block of `createResponse`, without noticing that
the earlier drive-by fix (6ebd41a1e) already declared it at the function
top (line 283). The second `const` creates a new block-scoped binding
inside the try, so earlier references within the same block (e.g.
line 348's `appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders`)
now hit the TDZ instead of the outer binding and throw
`ReferenceError: Cannot access 'appConfig' before initialization` —
which the outer try/catch then swallows into a generic 500.
This surfaced as all six token-usage tests in
`api/server/controllers/agents/__tests__/responses.unit.spec.js` failing
with `mockRecordCollectedUsage` never being called (because the throw
skips past the `recordCollectedUsage(...)` call).
Dropping the inner re-declaration restores the full control flow. All 11
tests in the file pass again.
* 🧹 refactor: Address review nits on Phase 9 housekeeping
- Move the `defaultAgentCapabilities` regression test out of the
`createEndpointsConfigService` describe block and into its own
top-level describe. It tests a module constant and has no relationship
to the service factory; nesting was misleading and made it easier to
accidentally drop if the service tests are later restructured.
- Re-order local imports in `SkillsAccordion.tsx` longest-to-shortest
per AGENTS.md convention (`SkillsSidePanel` 48 chars before
`useAuthContext` 41 chars).
Post-merge sanity-review cleanup on top of #12746:
- `createSkill` / `updateSkill` now parse SKILL.md body's always-apply
status once and reuse it for both validation and derivation (was
parsing the same YAML block twice per call).
- Body-inline `always-apply:` validation becomes precedence-aware: a
caller sending an explicit top-level `alwaysApply` or a structured
`frontmatter['always-apply']` no longer gets rejected for a typo in
the body — the body value is never consulted at derivation time when
a higher-precedence source wins. New tests cover the three relevant
interactions (explicit+body-typo, frontmatter+body-typo, body-only
typo still rejects).
- OpenAI and Responses controllers now emit a `logger.warn` when
`injectSkillPrimes` drops always-apply primes to stay under
`MAX_PRIMED_SKILLS_PER_TURN`. `injectSkillPrimes` already logs
internally; the controller-level warn adds endpoint context so
operators can identify which path hit the cap at a glance. Mirrors
AgentClient's existing log.
- Rename `ManualSkillPills` → `SkillPills` (component + type + file +
test + all JSDoc references). The component handles both manual and
always-apply pills now; the original name was carried over from the
manual-only Phase 3 and misleads new readers.
- Drive-by fix: declare `appConfig = req.config` at the top of
`createResponse` in `responses.js` — it was used unqualified on
lines 381/396, which silently evaluated to `undefined` (via optional
chaining) and disabled the skills-capability check on the Responses
endpoint. Pre-existing, surfaced by lint on the touched file.
* 🔌 refactor: Decouple bash_tool from Per-User CODE_API_KEY
Phase 4 of Agent Skills umbrella (#12625): gate bash_tool and skill
file priming on the `execute_code` capability only. Thread a boolean
`codeEnvAvailable` through `enrichWithSkillConfigurable` and
`primeInvokedSkills` in place of the old per-user `codeApiKey` +
`loadAuthValues` plumbing. The sandbox API key is the LibreChat-
hosted service key — system-level, not a user secret — so the
per-user lookup was legacy; when needed, it's read directly from
`process.env[EnvVar.CODE_API_KEY]` inside the capability gate.
`handleSkillToolCall` and `primeInvokedSkills` gate sandbox uploads
on `codeEnvAvailable` first, preventing skill-file uploads to the
sandbox when an agent has `execute_code` disabled even if the env
var happens to be set. The agents library resolves the env key
itself for `bash_tool`, so `ToolService.js` drops the
`loadAuthValues` lookup and the "Code execution is not available"
placeholder tool in favor of a plain `createBashExecutionTool({})`
with a loud error log if the env var is missing.
Also fixes a pre-existing `appConfig`-undefined lint error in
`responses.js`/`createResponse` that surfaced when this file was
touched (declares `const appConfig = req.config` at function top,
matching the existing pattern in other controllers).
Preserves the `skillPrimedIdsByName` threading added by Phase 3/5/6
and all Phase 3/5/6 call-site signatures. Adds
`skillConfigurable.spec.ts` (5 cases pinning the new surface) and
`skillFiles.spec.ts` (4-way matrix of capability × env key for
`primeInvokedSkills`).
* 🧪 refactor: Address Codex Review Feedback
Resolves findings from the second codex review on #12712:
- MAJOR: `handlers.spec.ts` now covers the `codeEnvAvailable` gate in
`handleSkillToolCall` across three cases (gate off, gate on + env
set, gate on + env unset). The gate is the critical regression
prevention — a future edit that drops it would silently re-enable
sandbox uploads for agents with `execute_code` disabled.
- MINOR: Hoist `codeEnvAvailable` and `skillPrimedIdsByName` out of
`loadTools` closures in `openai.js` and `responses.js`. Both values
are fixed once `initializeAgent` resolves, so recomputing them on
every tool execution was wasted work. `responses.js` shares a single
pair between its streaming and non-streaming branches.
- MINOR: `skillFiles.spec.ts` now has a test that exercises the full
upload path end-to-end with real file records, asserting
`batchUploadCodeEnvFiles` is called with the env-sourced apiKey and
the correct file set (including the synthetic `SKILL.md`).
- NIT: Finish the `appConfig` extraction in `responses.js/createResponse`
— replaces the remaining `req.config` references with `appConfig` for
consistency with the pattern in other controllers.
No behavioral changes beyond what was already in place; this is
coverage and readability polish.
* 🧷 test: Tighten Spec Hygiene Per Codex Nit Feedback
Round-3 codex review flagged two NITs on the test code added in the
previous commit:
- Replace `_id: 'skill-id' as unknown as never` in the new
`makeSkillHandlerWithFiles` helper with a real `Types.ObjectId`,
matching the pattern used by the primed-skill tests further up in
the same file (and by `skillFiles.spec.ts`). The `never` cast
hides the fact that `_id` really is a string / ObjectId at runtime.
- Replace the ad-hoc `{ on, pipe, read }` stub with a real
`Readable.from(Buffer.from(''))` in the upload-path test. The stub
worked only because `batchUploadCodeEnvFiles` is mocked and never
iterates the stream; `Readable.from` satisfies the same contract
and is robust to any future partial-real replacement of the upload
function.
Pure test-hygiene improvements; no runtime code touched.
* 🧹 chore: Remove Duplicate appConfig Declaration After Rebase
The upstream `6ebd41a1e` fix declared `const appConfig = req.config`
inside the try block (line 381) to patch the same `no-undef` error
I fixed in this PR at the top of `createResponse` (line 283). The
rebase kept both declarations side-by-side. Drops the inner one —
the outer binding covers every downstream reference already.
responses.js referenced `appConfig` on lines 381 / 396 without ever
declaring it, so `createResponse` threw `ReferenceError: appConfig is
not defined` the moment it entered the skills-capability block. The
existing `recordCollectedUsage` unit tests silently stopped running
(try/catch swallowed the error into `logger.error`), so CI showed
6 assertions failing with "Expected calls: 1, Received: 0" — the
function never reached the recorder.
Mirror initialize.js: seed `appConfig = req.config` at the top of the
try block, before the `enabledCapabilities` Set it feeds into. The two
later `appConfig: req.config` call-sites keep the direct reference —
only the lexical reads needed a binding.
This failure already exists on origin/feat/agent-skills (the same 6
tests fail there with the same stack) but blocks our branch too since
we're rebased on top, so fix it here and cherry-pick back if needed.
* 🔁 refactor: Rebase always-apply work onto merged structured-frontmatter columns
Phase 6 (disable-model-invocation / user-invocable / allowed-tools)
landed first on feat/agent-skills. Reconcile this branch with the new
mainline:
- Thread alwaysApplySkillPrimes through unionPrimeAllowedTools alongside
manualSkillPrimes, applying the combined MAX_PRIMED_SKILLS_PER_TURN
ceiling before loading tools.
- Add `_id` to ResolvedAlwaysApplySkill to match Phase 6's
ResolvedManualSkill shape (read_file name-collision protection).
- Register 'always-apply' in ALLOWED_FRONTMATTER_KEYS / FRONTMATTER_KIND
so Phase 6's validator recognizes it.
- Drop frontmatter from the listSkillsByAccess projection; the backfill
helper remains as defensive code but its read path is no longer
exercised on summary rows (no legacy rows exist — the branch never
shipped), saving ~200KB per page.
- Retire the corresponding "backfills legacy on summaries" test.
- Plumb listAlwaysApplySkills through the JS controllers + endpoint
initializer so the always-apply resolver sees a real DB method.
* 🧹 fix: Dedupe manual/always-apply overlap, share YAML util, tidy comments
Addresses review findings:
- Cross-list dedup: when a user $-invokes a skill that is also marked
always-apply, the always-apply copy is now dropped so the same
SKILL.md body never primes twice in one turn. Manual wins (explicit
intent, closer to the user message). Dedup runs in both
initializeAgent (so persisted user-bubble pills stay in sync) and
injectSkillPrimes (defense-in-depth at splice time). New test cases
cover single-overlap, partial-overlap, and dedup-before-cap.
- DRY: extract stripYamlTrailingComment to
packages/data-schemas/src/utils/yaml.ts; packages/api/src/skills/import.ts
now imports the shared helper. Also drop the redundant inner
stripYamlTrailingComment call inside parseBooleanScalar — the call
site already strips.
- Mark injectManualSkillPrimes as @deprecated in favor of
injectSkillPrimes (kept for external consumers of @librechat/api).
- Document SKILL_TRIGGER_MODEL as forward-looking plumbing for the
model-invoked path rather than leaving it as a bare unused export.
- Replace the stale "frontmatter is included" comment on
listSkillsByAccess with an accurate explanation of why it was
intentionally excluded.
* 🔒 fix: Include always-apply primes in skillPrimedIdsByName + clear alwaysApply on body opt-out
Two bugs flagged by Codex review:
P1 (read_file): `manualSkillPrimedIdsByName` only carried manual-invocation
primes, so an always-apply skill with `disable-model-invocation: true`
was blocked from reading its own bundled files, and same-name collisions
could resolve to a different doc than the one whose body got primed.
- Rename `buildManualSkillPrimedIdsByName` → `buildSkillPrimedIdsByName`
(accepts both manual + always-apply prime arrays).
- Rename the configurable field `manualSkillPrimedIdsByName` →
`skillPrimedIdsByName` throughout the plumbing (skillConfigurable.ts,
handlers.ts, CJS callers, tests).
- Overlap resolution: manual wins on the rare edge case where the same
name appears in both arrays (upstream dedup should prevent this, but
defensive merging treats manual as authoritative).
- New tests: (1) gate-relaxation fires for always-apply primes, (2) `_id`
pinning works for always-apply same-name collisions.
P2 (updateSkill): when a body update had no `always-apply:` key,
`extractAlwaysApplyFromBody` returned `absent` and the column was left
untouched. A skill that was once `alwaysApply: true` would keep
auto-priming even after its SKILL.md no longer declared the flag.
- Treat `absent` as a positive "not always-apply" declaration when the
body is explicitly submitted; flip the column to `false`.
- Explicit top-level `alwaysApply` still wins (three-source precedence
unchanged).
- New tests: body removes key → false, body has no frontmatter at all →
false, explicit + body-without-key → explicit wins.
* 🧵 refactor: Collapse duplicate prime types + tighten parse + test hygiene
Sanity-check review follow-ups:
- Collapse `ResolvedManualSkill` / `ResolvedAlwaysApplySkill` into a
single `ResolvedSkillPrime` canonical interface with two backward-
compatible type aliases. Both resolvers feed the same pipeline stages
(injectSkillPrimes, unionPrimeAllowedTools, buildSkillPrimedIdsByName);
the per-source distinction lives on `additional_kwargs.trigger`, not
on the resolver output.
- Move the `always-apply` branch in `parseFrontmatter` to operate on the
raw post-colon text. The outer `unquoteYaml` was fine today because
it's idempotent on non-quoted strings, but running it twice (once per
line, once after stripping the inline comment) would be fragile if the
unquoter ever grows richer YAML-escape handling.
- Add the missing `alwaysApplyDedupedFromManual: 0` field to the
`injectSkillPrimes` mocks in `openai.spec.js` and `responses.unit.spec.js`
so they match the full `InjectSkillPrimesResult` contract.
- Insert the blank line between the `unionPrimeAllowedTools` and
`resolveAlwaysApplySkills` describe blocks.
* 🔧 fix(tsc): Cast mock.calls via `unknown` for strict tuple destructure
`getSkillByName.mock.calls[0]` is typed as `[]` by jest's generic default;
a direct cast to `[string, ..., ...]` fails TS2352 under `--noEmit` even
though the runtime shape matches. Go through `as unknown as [...]` like
the earlier test in the same file so CI's type-check step stays green.
* 🪢 fix: Propagate skillPrimedIdsByName into handoff agent tool context
Handoff agents go through the same `initializeAgent` flow as the primary
(with `listAlwaysApplySkills` now plumbed), so they resolve their own
`manualSkillPrimes` and `alwaysApplySkillPrimes` — but the
`agentToolContexts.set(...)` for handoff agents didn't carry
`skillPrimedIdsByName` into the per-agent context.
That meant `handleReadFileCall` fell back to the full ACL set + a
`prefer*` flag for handoff agents: same-name collisions could resolve to
a different doc than the one whose body got primed, and a
`disable-model-invocation: true` skill primed via manual `$` or
always-apply inside the handoff flow would be blocked from reading its
own bundled files.
Build the map via `buildSkillPrimedIdsByName(config.manualSkillPrimes,
config.alwaysApplySkillPrimes)` for every handoff tool context so
`read_file` behaves identically across primary and handoff agents.
* 🧬 feat: Persist `disable-model-invocation` / `user-invocable` / `allowed-tools`
Adds first-class columns mirroring the three runtime-enforced frontmatter
fields, with a `deriveStructuredFrontmatterFields` helper that maps from
frontmatter at create/update time and re-syncs (via `$unset`) when fields
are removed. `listSkillsByAccess` projection includes them so the Phase 6
catalog filter and popover filter can both read off the summary row.
Marks `invocationMode` as @deprecated on `TSkill` and the
`InvocationMode` enum — the runtime now reads the persisted pair instead.
* 🛡️ feat: Enforce frontmatter at runtime (catalog, skill tool, manual resolver, tool union)
Wires the persisted columns into actual runtime behavior across all four
invocation paths:
- `injectSkillCatalog` excludes `disableModelInvocation: true` skills
before catalog formatting — they cost zero context tokens and stay
invisible to the model.
- `handleSkillToolCall` rejects with a clear error when the model names
a skill marked `disable-model-invocation: true` (defends against a
stale-cache or hallucinated invocation getting past the catalog
filter).
- `resolveManualSkills` skips `userInvocable: false` skills with a warn
log so an API-direct caller can't bypass the popover-side filter.
- `unionPrimeAllowedTools` collects skill-declared `allowed-tools` minus
what's already on the agent; `initialize.ts` re-runs `loadTools` for
the extras and merges resulting `toolDefinitions` into the agent's
effective set for the turn. Tool-name resolution is tolerant —
unknown names silently drop with a debug log so cross-ecosystem
skills referencing yet-to-be-implemented tools (Claude Code's
`edit_file`, etc.) import without breaking. The agent document is
never modified; the union is turn-scoped.
Helper exports (`unionPrimeAllowedTools`) are structured so Phase 5's
always-apply primes flow through the same union (combined
`[...manualPrimes, ...alwaysApplyPrimes]`) once the resolver lands.
Skill handler wire format gains the three fields so clients can render
them on detail / list views.
* 🎛️ feat: `$` popover reads `userInvocable` instead of UI-only `invocationMode`
Replaces the phase-1 UI-only `invocationMode` check with the persisted
`userInvocable` field (mirrors the `user-invocable` frontmatter). Skills
authored with `user-invocable: false` no longer surface in the popover;
the backend resolver enforces the same rule for defense-in-depth.
Default-visible behavior is preserved: skills without an explicit
`userInvocable` value (older rows, freshly imported skills that don't
declare the field) stay visible — only an explicit `false` hides them.
Test fixture updated to reflect the new field.
* 🔧 fix: Address Phase 6 review findings
Codex P2 + reviewer #1: Single `loadTools` call with the union of
`agent.tools + allowed-tools`. The earlier two-call approach dropped
`userMCPAuthMap` / `toolContextMap` / `actionsEnabled` from the
skill-added pass — an MCP tool gained via `allowed-tools` would be
visible to the model but fail at execution without per-user auth
context. Resolution of `manualSkillPrimes` is hoisted before
`loadTools` so the union can be computed up-front; the dropped-tools
debug log now compares loaded vs. requested across the single call.
Codex P3 + reviewer #2: `injectSkillCatalog.activeSkillIds` now
includes `disable-model-invocation: true` skills. The runtime ACL
check in `handleSkillToolCall` previously couldn't reach the explicit
"cannot be invoked by the model" rejection because the broader access
set excluded those skills. Catalog text and tool registration still
gate on the visible subset (zero-context-token guarantee preserved);
only the per-user `isActive` filter is a hard exclusion now.
Reviewer #1 (try/catch around loadTools, MAJOR): A single bad
`allowed-tools` entry from a shared skill could crash the entire turn.
Now wrapped — on failure with extras, retry with just `agent.tools`
and continue (the dropped-tools debug log surfaces what vanished). If
the retry-without-extras still throws, propagate; the agent's own
tools are the load-bearing surface.
Reviewer #3 (integration tests, MAJOR): Added six tests in
`initialize.test.ts` covering the full `allowed-tools` loading path:
union pass-through, no-extras short-circuit, agent-baseline dedup,
loadTools throw + retry, propagated throw without extras, and the
empty-tools edge case.
Smaller cleanups bundled in:
- Reviewer #4: Moved `logger` import to the package-imports section
(was wedged among local imports).
- Reviewer #5: Removed unused index on `disableModelInvocation`
(filtering happens application-side in `injectSkillCatalog`; index
cost write overhead for zero query benefit).
- Reviewer #6: Swapped order of `userInvocable` and body checks in
`resolveManualSkills` so the more authoritative author-decision
reason surfaces first when both apply.
- Reviewer #8: Documented the `allowedTools` enforcement gap on the
schema + type — model-invoked skills (mid-turn `skill` tool calls)
do NOT trigger tool union, since adding tools after the graph
starts would require a rebuild. Manual / always-apply (Phase 5)
primes are the supported paths.
- Reviewer #9: Renamed `dmi` / `ui` / `at` locals to
`disableModelInvocationRaw` / `userInvocableRaw` / `allowedToolsRaw`
in `deriveStructuredFrontmatterFields`.
Reviewer #7 (DRY shared `getSkillByName` return type) deferred —
field sets diverge meaningfully across the three call sites (handler
needs `body + fileCount`; resolver needs `author + allowedTools +
userInvocable`; the InitializeAgentDbMethods contract needs the
superset). A `Pick<>`-based consolidation is a follow-up cleanup.
* 🔧 fix: Address codex iter 2 — catalog quota + duplicate-name dedup
P1: `injectSkillCatalog` cap now counts only model-visible skills, not
the merged active set. The previous behavior let a tenant with many
`disable-model-invocation: true` rows near the top of the cursor
exhaust the 100-slot quota before any invocable skill got scanned —
the catalog could end up empty even though invocable skills existed
further down the paginated results. `MAX_CATALOG_PAGES` stays the
ceiling on scan budget; only `visibleCount` drives the early-exit on
quota fill.
P2: When an invocable and a `disable-model-invocation: true` skill
share a name, drop the disabled doc(s) from `activeSkillIds`. Without
this dedup, `getSkillByName` (which sorts by `updatedAt` desc) could
pick the disabled doc and every model call to the cataloged name
would fail with "cannot be invoked by the model" instead of executing
the visible skill. When ONLY a disabled doc exists for a name, it
stays in `activeSkillIds` so the explicit-rejection error path still
fires for hallucinated invocations.
Tests: 3 new cases in `injectSkillCatalog` covering (a) cap counted
on visible skills only, (b) same-name collision drops disabled doc,
(c) sole-disabled-name case keeps the disabled doc.
* 🔒 fix: Apply `disable-model-invocation` gate to read_file too (codex iter 3 P1)
`activeSkillIds` is shared between the `skill` and `read_file` handlers.
The skill-tool gate was applied last iteration, but `handleReadFileCall`
authorized purely on `getSkillByName(..., accessibleIds)` — so a model
that learned a hidden skill's name (stale catalog or hallucination)
could still read its `SKILL.md` body or bundled files via `read_file`,
defeating the contract. Same explicit rejection now fires from both
handlers; no change needed to the ACL set itself (disabled docs stay
in `activeSkillIds` so the explicit error path keeps firing).
Two new tests in `handlers.spec.ts` cover the read_file gate and
regression-protect the happy path.
* 🔧 fix: Address codex iter 4 — manual-prime exception + legacy frontmatter backfill
P1: Scope the `read_file` `disableModelInvocation` gate to AUTONOMOUS
model probes only. A user-invoked `$` skill that is also marked
`disable-model-invocation: true` had its bundled `references/*` /
`scripts/*` files unreadable, leaving the manually-primed skill body
referencing files the model couldn't load. Now the handler bypasses
the gate when the skill name appears in `manualSkillNames` (the
per-turn allowlist threaded from `manualSkillPrimes` →
`agentToolContexts` → `enrichWithSkillConfigurable` →
`mergedConfigurable`). Defense-in-depth: the bypass is scoped to the
specific names in the allowlist; a different disabled skill name is
still rejected.
P2: Read-time fallback for legacy skills authored before Phase 6
landed the structured columns. `user-invocable: false` /
`disable-model-invocation: true` set in `frontmatter` (the validator
already accepted those keys) but with no derived column would
incorrectly evaluate as "user-invocable / model-allowed" until a save
backfilled the columns. New `backfillDerivedFromFrontmatter` helper
fills undefined columns from frontmatter at read time in both
`getSkillByName` and `listSkillsByAccess` — column wins when both are
set, frontmatter fills the gap when only it's set. No DB writes; the
next `updateSkill` naturally persists. `listSkillsByAccess` projection
expanded to include `frontmatter` (bounded by validator, payload
impact small) so summaries can also be backfilled.
Sticky-primed disabled skills (ones invoked in prior turns of the
same conversation) are not yet in the manual-prime allowlist — same-
turn manual invocation is the load-bearing path codex flagged; the
sticky-turn case is a known limitation tracked for a follow-up.
Tests: 2 new in handlers.spec.ts (manual-prime allows + name-scoped
block holds), 3 new in skill.spec.ts (legacy backfill via
getSkillByName + listSkillsByAccess + column-wins precedence).
* 🔧 fix: Address codex iter 5 — propagate manualSkillNames + keep read_file
P1: `enrichWithSkillConfigurable` is also called from `openai.js` and
`responses.js` (the OpenAI Responses + completions endpoints). Both
were ignoring the new `manualSkillNames` parameter, which meant the
manual-prime exception in the `read_file` gate (iter 4) only worked
on the agents endpoint. Now all three call sites pass
`primaryConfig.manualSkillPrimes?.map(p => p.name)` so manual `$`
invocations of disabled skills work consistently across endpoints.
P2: When every accessible skill is `disable-model-invocation: true`,
the catalog text and `skill` tool are correctly omitted (no model-
reachable targets) — but `read_file` and `bash_tool` MUST still be
registered. A user manually invoking such a skill gets its SKILL.md
body primed into context; if the body references `references/foo.md`
or `scripts/run.sh`, those reads need a registered tool. Restructured
`injectSkillCatalog` so `skill` registration is gated on
`catalogVisibleSkills.length > 0` while `read_file` (always) and
`bash_tool` (when codeEnvAvailable) register whenever any active
skill is in scope.
Tests: existing all-disabled test rewritten to assert read_file IS
registered + skill is NOT; new test confirms bash_tool joins it
when codeEnvAvailable.
* 🔧 fix: Address codex iter 6 — name-collision consistency via preferInvocable
P2a (resolveManualSkills): a name collision between an older
user-invocable doc and a newer non-user-invocable doc made manual `$`
invocation silently no-op. The popover surfaced the older invocable
doc; resolver looked it up by name; `getSkillByName` returned the
newer non-invocable doc; resolver skipped on `userInvocable: false`.
P2b (handler / runtime ACL): with same-name duplicates (e.g. older
invocable + newer disabled), the manual prime resolved to one doc
while later `read_file` / `skill` execution resolved a different doc
through `activeSkillIds`. Model could follow one SKILL.md body while
reading files from a different skill.
Both root-cause: `getSkillByName` always returned the newest match
and let the caller filter, but with collisions the newest can be
something the caller didn't want.
Fix: extend `getSkillByName` with `options.preferInvocable`. When
true, prefer the newest doc satisfying BOTH `userInvocable !== false`
AND `disableModelInvocation !== true` (with frontmatter backfill);
fall back to the newest match otherwise. Fast path preserved when
caller doesn't opt in.
Callers passing `preferInvocable: true`:
- `resolveManualSkills` — picks the popover-visible invocable doc
even when a newer disabled / non-user-invocable duplicate exists.
- `handleSkillToolCall` — keeps execution aligned with the catalog;
falls back to the disabled doc only when no invocable variant
exists (so the explicit "cannot be invoked by the model" gate
still fires for the hallucinated-disabled-name case).
- `handleReadFileCall` — same alignment, plus the manual-prime
exception added in iter 4 still applies.
Tests:
- 2 new in skill.spec.ts (preferInvocable picks invocable when
collision exists; falls back to newest when no clean-invocable
exists).
- 1 new in skills.test.ts (resolver passes preferInvocable through).
- 2 new in handlers.spec.ts (skill tool + read_file pass it).
- Existing initialize.test.ts assertion updated for the new option.
* 🔧 fix: Address codex iter 7 — split preferInvocable into per-axis flags
The previous unified `preferInvocable` filter required BOTH
`userInvocable !== false` AND `disableModelInvocation !== true`. That
was wrong for the model paths: `userInvocable: false` skills are
model-only and remain valid `skill` / `read_file` invocation targets.
A duplicate-name scenario where the newer cataloged doc was model-
only would let the older user-invocable variant shadow it on every
model call.
Split the option into two independent axes:
- `preferUserInvocable` — for manual paths (`$` popover). Skips docs
with `userInvocable: false`. Disable-model-invocation status is
irrelevant; iter 4 explicitly supports manual prime of disabled
skills.
- `preferModelInvocable` — for model paths (`skill` / `read_file`
handlers). Skips docs with `disableModelInvocation: true`. User-
invocable status is irrelevant; model-only skills are valid here.
Both flags fall back to the newest match when no preferred doc
exists, so the explicit-rejection error paths still fire correctly
in the sole-disabled-name case.
Callers updated:
- `resolveManualSkills` → `preferUserInvocable: true`
- `handleSkillToolCall` / `handleReadFileCall` → `preferModelInvocable: true`
Tests:
- New spec test for preferModelInvocable not filtering on userInvocable.
- Existing preferInvocable test renamed/split to cover the new axes.
- New test asserts preferUserInvocable still returns disabled docs
(preserves iter 4 manual-disabled support).
- Caller tests assert each path passes the right single flag and
does NOT pass the wrong one.
* 🔧 fix: TypeScript type-check failure in handlers.spec.ts (CI green)
`jest.fn(async () => ...)` without explicit args infers an empty tuple
for the call signature, so `mock.calls[0][2]` flagged as "Tuple type
'[]' has no element at index '2'." Cast to `unknown[]` then narrow to
the expected option shape. Behavior unchanged.
Caught by the `Type check @librechat/api` CI step
(.github/workflows/backend-review.yml).
* 🔧 fix: Address codex iter 8 — undefined-result fallback + read_file alignment
P1 (loadTools returning undefined): Production loaders
(`createToolLoader` in `initialize.js` / `openai.js` /
`responses.js`) wrap `loadAgentTools` in try/catch and return
`undefined` on failure rather than throwing. Without explicit
handling, my iter-1 try/catch only fired for thrown errors — a
silent-failure on a skill-added tool would fall through to the
empty fallback and silently DROP the agent's baseline tools for
the turn (much worse than just losing the extras). Added an
`undefined`-result branch that retries with just `agent.tools`,
mirroring the throw branch. Test pins both behaviors.
P2 (read_file alignment with manual prime): When a skill is in
this turn's `manualSkillNames`, the `read_file` handler now uses
`preferUserInvocable` instead of `preferModelInvocable`. Same
name-collision rule as `resolveManualSkills`, so the doc whose
files get read is the same doc whose body got primed. For
autonomous probes (skill not in `manualSkillNames`), the handler
keeps `preferModelInvocable` to align with the catalog the model
saw. Two new tests cover both branches and regression-protect that
the wrong flag isn't passed.
* 🔧 fix: Address codex iter 9 — pin read_file lookup to primed skill _id
P1 (manually-primed disabled IDs were dropped from activeSkillIds):
The `executableSkills` dedup in `injectSkillCatalog` correctly drops
`disable-model-invocation: true` duplicates when an invocable doc
shares the name — but `resolveManualSkills` legitimately primes
disabled docs (iter 4 supports manual `$` invocation of disabled
skills). When the resolver primed a disabled doc, the read_file
handler couldn't find it in the (deduped) `activeSkillIds` and
either resolved a different same-name skill or returned not-found.
Fix: `ResolvedManualSkill` now carries `_id`; the legacy `initialize.js`
/ `openai.js` / `responses.js` controllers build a
`manualSkillPrimedIdsByName` map and `enrichWithSkillConfigurable`
passes it into `mergedConfigurable`. `handleReadFileCall` now pins
its lookup's `accessibleIds` to `[primedId]` whenever the requested
skill is in the map. The constrained set guarantees the lookup
returns the EXACT doc the resolver primed — body/files come from the
same source even when same-name duplicates exist or the dedup
removed the prime's id from `activeSkillIds`.
Autonomous read_file probes (skill not in the manual-primed map)
keep the full ACL set + `preferModelInvocable` so they align with
the catalog the model saw and the disabled-only case still fires
the explicit-rejection gate.
Test fixture changes flow from `_id` becoming required on
`ResolvedManualSkill`. `buildSkillPrimeContentParts` /
`injectManualSkillPrimes` widen their param types to `Pick<...>`
because they only read `name` / `body` and shouldn't force test
literals to invent placeholder ids.
* 🧹 fix: Address independent reviewer findings (DRY + types + tests + docs)
Sanity-pass review surfaced 7 findings; addressed 6 (the 7th — DRY
on inline `getSkillByName` return types — is acknowledged tech debt
deferred to a follow-up).
#1 [MAJOR, DRY]: The 4-line `manualSkillPrimedIdsByName` map
construction was duplicated across 4 CJS call sites (openai.js,
responses.js x2, initialize.js). Extracted `buildManualSkillPrimedIdsByName`
helper in `skillDeps.js`; all four sites now call the helper. If
`ResolvedManualSkill` ever renames `_id` or gains identifying fields,
only the helper changes.
#2 [MINOR, type safety]: `handleReadFileCall` was casting a hex string
to `Types.ObjectId[]` via `as unknown as`, relying on mongoose's
auto-cast in `$in` queries. Replaced with `new Types.ObjectId(...)`
so any future consumer comparing with `.equals()` / `===` gets the
correct value type. Imported `Types` as a value (was type-only).
#5 [MINOR, test gap]: Added a test for the worst-case silent-failure
path — both the union and base-only `loadTools` calls return undefined.
The agent gets no tools but the turn doesn't crash hard; pinning
that contract.
#4 [MINOR, performance]: Added a TODO on the `listSkillsByAccess`
projection noting the `frontmatter` field can be dropped once a
write migration backfills all pre-Phase-6 skills' columns. ~2KB/skill
× 100/page is wasted bandwidth post-backfill.
#6 [NIT, docs]: `backfillDerivedFromFrontmatter` JSDoc said "Pure"
right before "mutates its undefined fields in place". Replaced with
"Side-effect-free w.r.t. the DB (no writes), but mutates its argument
in place" which describes both halves accurately.
#7 [NIT, test determinism]: Replaced `await new Promise(r => setTimeout(r, 5))`
in two same-name collision tests with explicit `updateOne` setting
`updatedAt: new Date(Date.now() - 1000)` on the older doc. Removes
the wall-clock race on fast CI runners. The pagination test (line
480) still uses setTimeout — that test is pre-existing and order
is incidental, not load-bearing.
Existing test fixtures updated to use valid 24-char hex ObjectIds
(required by the iter-9 test that constructs a real `ObjectId`).
#3 [MINOR, deferred]: Inline `getSkillByName` return type duplicated
across `handlers.ts`, `initialize.ts`, `skills.ts`. Reviewer
acknowledged this as deferred; field sets diverge across call sites
(handler needs `fileCount`, resolver needs `author`/`allowedTools`).
A `Pick<>`-based consolidation is a clean follow-up.
* 🪜 chore: Plumb `allowedTools` through `resolveManualSkills`
Tiny shape-only precursor shared by Phase 5 (`always-apply`) and Phase 6
(frontmatter runtime enforcement). Adds `allowedTools?: string[]` to
`ResolvedManualSkill` and widens the `getSkillByName` return type in
`ResolveManualSkillsParams` and `InitializeAgentDbMethods` to carry the
same field. The resolver forwards `skill.allowedTools` verbatim when
present.
No runtime behavior change — the field is populated but not yet consumed.
Phase 6 will union the per-skill allowlist into the agent's effective
tool set for the turn; Phase 5's `ResolvedAlwaysApplySkill` will mirror
this shape. Landing this first eliminates the type-shape race between
the two phases.
* 🪜 style: Drop phase-name refs from `allowedTools` JSDoc for longevity
JSDoc that cites "Phase N will X" goes stale the moment that phase ships.
Swap for "future runtime enforcement" so the docs age with the code.
* 🎬 feat: Prime Manually-Invoked Skills via $ Popover
Lands the backend for manual skill invocation, making the $ popover
deterministically prime SKILL.md before the LLM turn instead of leaving
the model to discover the skill via the catalog.
Flow: popover drains pendingManualSkillsByConvoId on submit, attaches
names to the ask payload, controllers forward to initializeAgent, and
initialize resolves each name to its body (ACL + active-state filtered,
reusing the same rules as catalog injection). AgentClient splices the
primes as meta HumanMessages before the user's current message.
- Extract primeManualSkill / resolveManualSkills in packages/api/src/agents/skills.ts
and reuse primeManualSkill inside handleSkillToolCall for a single shape source.
- Thread manualSkills + getSkillByName through InitializeAgentParams / DbMethods
and all three initializeAgent call sites (initialize.js, responses.js, openai.js).
- Splice HumanMessage primes in client.js chatCompletion after formatAgentMessages,
shifting indexTokenCountMap so hydrate still fills fresh positions correctly.
- Carry isMeta / source / skillName in additional_kwargs for downstream filtering.
* 🛡️ fix: Scope manual skill primes to single-agent + cap resolver input
Two follow-ups to the Phase 3 priming path flagged in Codex review.
Multi-agent runs: skipping the splice when agentConfigs is non-empty.
`initialMessages` is shared across every agent in `createRun`, so splicing
a skill body there would bypass Phase 1's per-agent `scopeSkillIds`
contract — a handoff / added-convo agent with a different skill scope
would see content its configuration excludes. Warn + skip is the minimal
correct behavior; lifting this to per-agent initial state is a follow-up.
Input bounding: `resolveManualSkills` now truncates to `MAX_MANUAL_SKILLS`
(10) after dedup, with a warn listing the dropped tail. Controllers only
validate `Array.isArray(req.body.manualSkills)`, so a crafted payload
could otherwise fan out into an unbounded `Promise.all` of concurrent
`getSkillByName` DB lookups. Cap lives in the resolver so every caller
(including future `always-apply` in Phase 5) inherits it.
* 🧪 refactor: Testable Helpers + Payload Validation for Manual Skill Primes
Follow-ups from the comprehensive review. No behavior change for the
happy path — these are architectural and defensive improvements that
shrink the JS surface in /api, tighten the request-body contract, and
cover the delicate splice logic with proper unit tests.
- Extract `injectManualSkillPrimes` into packages/api/src/agents/skills.ts
so the message-array splice and `indexTokenCountMap` shift are unit-
testable in TS. client.js now calls the helper. Tests pin the `>=`
vs `>` boundary condition — a regression here would silently corrupt
token accounting for every message after the insertion point.
- Extract `extractManualSkills(body)` and use in all three controllers
(initialize.js, responses.js, openai.js). Replaces copy-pasted
`Array.isArray(...) ? ... : undefined` with a helper that also filters
non-string / empty elements — closes a type-safety gap where a crafted
payload like `{"manualSkills": [123, {"$gt":""}]}` would otherwise reach
`getSkillByName` and waste DB round-trips.
- Rename `primeManualSkill` → `buildSkillPrimeMessage`. The helper serves
three invocation modes (`$` popover, `always-apply`, model-invoked);
the old name misled readers coming from `handleSkillToolCall`.
- Add `loadable.state === 'hasValue'` guard in `drainPendingManualSkills`
— defensive, since the atom has a synchronous `[]` default, but the
previous `.contents` cast would have been unsound under loading/error.
- Document why `resolveManualSkills` honors the active-state filter even
for explicit `$` selections (Phase 2 popover filter + API-direct
hardening).
- Remove stray `void Types;` in initialize.test.ts — `Types` is already
consumed elsewhere in that test.
* 🔖 refactor: Single source for the skill-message source marker
Export `SKILL_MESSAGE_SOURCE = 'skill'` and use it in both construction
paths that stamp skill-primed messages — `buildSkillPrimeMessage` (for
the model-invoked tool path) and `injectManualSkillPrimes` (for the
user-invoked splice path). Downstream filtering and telemetry read this
marker, so the two paths must agree; keeping the literal in one place
removes the risk of them drifting when Phase 5's `always-apply` adds a
third caller.
* ♻️ refactor: Drop Multi-Agent Guard + Review Polish
- Remove the multi-agent skip in `AgentClient.chatCompletion`. Leaking
primes to handoff / added-convo agents via shared `initialMessages` is
the agents SDK's concern to scope; this layer should just inject and
let the graph handle agent-scoped state. The guard was well-intended
but produced a silent-drop UX where `$skill` in a multi-agent run did
nothing.
- Bound the `[resolveManualSkills] Truncating ...` warn output to the
first 5 dropped names plus a count suffix. A malicious payload of
1000 names was previously spilling all ~990 names into the log line.
- Remove dead `?? []` from the `hasValue`-guarded loadable read in
`drainPendingManualSkills` — the atom always yields a string[] when
resolved, so the nullish fallback was unreachable.
- Reorder skills.ts imports to follow the style guide: value imports
shortest-to-longest (`data-schemas` → `langchain/core/messages` →
multi-line `@librechat/agents`), type imports longest-to-shortest.
* 🧠 fix: Strip Skill Primes from Memory Window + Unbreak CI Mocks
Two fixes after the last push.
CI unbreak: `responses.unit.spec.js` and `openai.spec.js` mock
`@librechat/api` and the mock didn't expose the new `extractManualSkills`
symbol, so every test in those files crashed before reaching the
`recordCollectedUsage` assertion. Added `extractManualSkills: jest.fn()`
returning `undefined` to both mocks; the controllers now no-op on
manualSkills as the tests expect.
Codex P2: `runMemory` passes `messages` straight through to the memory
processor, so after the splice in `injectManualSkillPrimes`, SKILL.md
bodies ride along as if they were real user chat. That pollutes memory
extraction with synthetic instruction content and crowds out real turns
from the window.
- Export `isSkillPrimeMessage(msg)` from `packages/api/src/agents/skills.ts`
— a predicate keyed on the shared `SKILL_MESSAGE_SOURCE` marker.
- Filter `chatMessages = messages.filter(m => !isSkillPrimeMessage(m))`
at the top of `runMemory` before the window-sizing logic. Keeps the
primes visible to the LLM (they still ride in `initialMessages`) but
invisible to the memory layer.
- 5 new tests for the predicate covering marker-present, plain messages,
different source, non-object inputs, and array filter integration.
* 📜 feat: Show Skill-Loaded Cards for Manually-Invoked Skills
The $ popover was priming SKILL.md bodies into the turn but leaving no
visible trace on the assistant response — from the user's view it looked
like the `$name ` cosmetic text did nothing. Now each manually-invoked
skill renders the same "Skill X loaded" tool-call card that model-invoked
skills already produce via PR #12684's SkillCall renderer.
Approach: post-run prepend to `this.contentParts`. The aggregator owns
per-step indices during the run, so pre-seeding collides; waiting until
`await runAgents(...)` returns lets the graph settle before synthetic
parts slot in at the front.
- Export `buildSkillPrimeContentParts(primes, { runId })` from
`packages/api/src/agents/skills.ts`. Returns completed tool_call parts
(`progress: 1`, args JSON-encoded with `{skillName}`, output matching
the model-invoked path's wording) that the existing `SkillCall.tsx`
renderer draws identically.
- In `AgentClient.chatCompletion`, prepend the built parts to
`this.contentParts` immediately after `await runAgents`. Persistence
and the final-event reconcile come for free — `sendCompletion` already
reads `this.contentParts` verbatim.
- Card ordering: skills appear first in the assistant message, reflecting
that priming ran before the LLM's turn.
Live-during-streaming cards are a separate follow-up — the graph's
index-based aggregator makes that a bigger lift and this change delivers
the core UX win without fighting the stream ordering.
6 new unit tests covering part shape, args JSON contract, output text,
unique IDs, empty input, and startOffset ID differentiation.
* ⚡ feat: Emit Optimistic Skill Cards + Wire Primes in OpenAI/Responses
Two follow-ups from testing.
Optimistic card emit: the main chat path was only showing "Skill X
loaded" cards at final-reconcile time, so the user saw nothing happen
until the stream finished. Now emit synthetic ON_RUN_STEP +
ON_RUN_STEP_COMPLETED events right before `runAgents` starts — same
pattern the MCP OAuth flow uses in `ToolService` — so the cards appear
immediately. The graph's content at index 0 may overwrite them during
streaming, but the post-run `contentParts` prepend (unchanged) restores
them on final reconcile.
OpenAI + Responses parity: both controllers were resolving
`manualSkillPrimes` via `initializeAgent` but never injecting them into
`formattedMessages` before the run. Manual invocation silently did
nothing on `/v1/chat/completions` and the Responses API path. Now both
call `injectManualSkillPrimes` on the formatted messages so the model
sees SKILL.md bodies on every path. LibreChat-style card SSE events
don't apply to these OpenAI-shaped responses, so the live-emit is
chat-path-only.
- Export `buildSkillPrimeStepEvents(primes, { runId })` from
`packages/api/src/agents/skills.ts`. Uses `Constants.USE_PRELIM_RESPONSE_MESSAGE_ID`
by default so the frontend maps events to the in-flight preliminary
response message, matching the OAuth emitter.
- In `AgentClient.chatCompletion`, emit via `sendEvent` (or
`GenerationJobManager.emitChunk` in resumable mode) after
`injectManualSkillPrimes` runs, before the LLM turn begins.
- Wire `injectManualSkillPrimes` into `openai.js` + `responses.js` after
`formatAgentMessages`. Refactored the destructure to `let` on
`indexTokenCountMap` so the injector's returned map is usable.
- 8 new unit tests covering the step-event builder: pair cardinality,
default/custom runId, TOOL_CALLS shape + JSON args, progress:1 on
completion, index ordering, stepId/toolCallId pairing, empty input.
* 🎯 fix: Route Skill Prime Events to the Real Response + Sparse-Array Offset
Two bugs in the optimistic-card emit from the last pass.
1. Wrong runId. The events used `USE_PRELIM_RESPONSE_MESSAGE_ID` (the
MCP OAuth pattern), but OAuth emits DURING tool loading — before the
real response messageId exists. By the time skill priming fires, the
graph is about to emit with `this.responseMessageId`, so the PRELIM
runId orphaned every card onto the client's placeholder response
entry in `messageMap`, separate from the one the LLM's events were
building. Net effect: cards never rendered mid-stream.
Now passing `this.responseMessageId` — the same ID `createRun`
receives — so synthetic and real steps land on the same `messageMap`
entry.
2. Index 0 collision. With the runId fixed, card-at-0 would have hit
`updateContent`'s type-mismatch guard when the LLM's text delta
arrived at the same index, suppressing the whole text stream.
New `SKILL_PRIME_INDEX_OFFSET` = 100 placed on both the live SSE
emit and the server-side `contentParts` assignment. Sparse array
during streaming renders as `[llm_text, ..., card]` (skip-holes via
`Array#filter` / `Array#map`). `filterMalformedContentParts` from
`sendCompletion` compacts to dense `[text, card]` before persistence,
so streaming UI and saved message agree on order — no finalize
reorder jank. Post-run switches from `contentParts.unshift` to
`contentParts[OFFSET + i] = part` to mirror the live placement.
- Add `startIndex` option to `buildSkillPrimeStepEvents` with
`SKILL_PRIME_INDEX_OFFSET` default. Export the constant from
`@librechat/api` so `client.js` can reuse it for the post-run splice.
- Update the existing index-ordering test to the new default and add a
new test for the explicit `startIndex` override.
* 🎗️ feat: Replace \$skill-name Text with Pills on the User Message
The `$skill-name ` cosmetic text the popover was inserting into the
textarea had two problems: it lingered in the user message forever (the
card is a more meaningful marker), and it implied that free-form text
invocation like \"\$foo help me\" should work — which it doesn't, and
supporting it would mean another parsing layer nobody asked for.
Dropped the textarea insertion. Visual confirmation after submit now
comes from a compact `ManualSkillPills` row on the user bubble that
self-extinguishes once the backend's live skill-card stream
(`buildSkillPrimeStepEvents` from the last commit) populates the sibling
assistant response. Multiple skills render as multiple pills — the atom
was already a string array, so multi-select works for free.
- `SkillsCommand.tsx`: select handler no longer writes to the textarea.
Still drops the trigger `$` via `removeCharIfLast`, still pushes to
`pendingManualSkillsByConvoId`, still flips `ephemeralAgent.skills`.
- `families.ts`: new `attachedSkillsByMessageId` atomFamily keyed by
user messageId. `useChatFunctions.ask` writes the drained skill list
here on every fresh submit (regenerate/continue/edit still skip).
- `ManualSkillPills.tsx` renders pills conditionally: hidden when the
message isn't a user message, when no skills are attached, or when
the sibling assistant response already carries a `skill` tool_call
content part (the live card took over). Reads messages via React Query
so we don't re-render on every message-state keystroke.
- `Container.tsx` mounts the pills above the user message text, parallel
to the existing `Files` slot.
- Updated the SkillsCommand select-flow spec to assert the textarea is
cleared of `$` instead of populated with `\$name `. 5 new tests for
`ManualSkillPills` covering empty state, non-user message guard,
multi-skill rendering, the skill-card hide condition, and the
text-only-content-doesn't-hide case.
* 🎛️ feat: Manual Skills as Persisted Message Field + Compose-Time Chips
Three problems with the previous pass:
1. Cards rendered BELOW the LLM text on the assistant message (and
stayed there on reload) because the sparse index-100 offset put them
after the model's content. Now back to `unshift` — cards at the top,
same as before the live-emit detour.
2. Pills on the user message disappeared the moment the live card
arrived, so users barely saw them. The live-emit channel also added
meaningful complexity and relied on a per-message Recoil atom that
had no clean cleanup story.
3. No visual cue at all during new-chat compose — the `$name ` text was
removed, the submitted-message pills weren't there yet, and the
popover closes after selection. User had no way to see what they'd
queued up before sending.
New architecture: `manualSkills` is a first-class field on `TMessage`,
persisted by the backend on the user message. `ManualSkillPills` reads
straight from `message.manualSkills` — no atom, no sibling-lookup — so
pills survive reload, show in history, and stay for the lifetime of the
message. Compose-time chips above the textarea read the existing
`pendingManualSkillsByConvoId` atom and let users × skills out before
submitting.
Backend reverts:
- `client.js`: dropped the `ON_RUN_STEP` live-emit loop, restored
`this.contentParts.unshift(...primeParts)` so cards sit at the top of
the persisted assistant response.
- `skills.ts`: removed `buildSkillPrimeStepEvents` and
`SKILL_PRIME_INDEX_OFFSET` (both unused now). `GraphEvents`,
`StepTypes`, and `Constants` imports went with them. Removed 8 tests.
Field persistence:
- `tMessageSchema` gains `manualSkills: z.array(z.string()).optional()`.
- Mongoose message schema gains `manualSkills: { type: [String] }` with
matching `IMessage` TS field.
- `BaseClient.js` reads `req.body.manualSkills` on user-message save,
filters to non-empty strings, pins onto `userMessage` before
`saveMessageToDatabase`. Mirrors the existing `files` pattern right
above it. Runtime resolution still reads top-level `req.body.manualSkills`
— persistence and resolution are separate concerns.
Frontend:
- `useChatFunctions.ask` sets `currentMsg.manualSkills` directly; the
drained atom value goes onto the message, not a separate atom.
Removed the `attachSkillsToMessage` Recoil callback.
- `ManualSkillPills`: pure render of `message.manualSkills`. No more
`useQueryClient`, no sibling scan, no atom read. Loses the
auto-hide-when-card-arrives behavior — pills stay on the user
bubble, cards live on the assistant bubble, both are informative.
- Dropped the `attachedSkillsByMessageId` atomFamily and its export.
- New `PendingManualSkillsChips` above the textarea reads the
compose-time atom and renders chips with × to remove. Mounted in
`ChatForm` right after `TextareaHeader`. Naturally hides on submit
when the atom drains.
Tests: updated `ManualSkillPills` suite to the new field-based reads
(5 passing). New `PendingManualSkillsChips` suite covering empty state,
multi-chip render, single × removal, and full-clear (4 passing).
Backend suite trimmed to 89 (was 97) from the step-events test
removal — no regressions on the remaining helpers.
* 🧪 feat: Assistant-Side Skill-Loading Chips + Pill Padding
Two small UX fixes on top of the field-on-message architecture.
Pill padding: bumped the user-side `ManualSkillPills` from `py-0.5` to
`py-1` on each chip and added `py-0.5` to the wrapper so the row
breathes a little without feeling tall.
Mid-stream indicator: new `InvokingSkillsIndicator` mirrors the parent
user message's `manualSkills` onto the assistant bubble as transient
"Running X" chips while the real card is in flight. Renders above
`ContentParts` in `MessageParts`. Hides itself when the assistant's
own `content` grows a `skill` tool_call — the authoritative card from
`buildSkillPrimeContentParts.unshift` is showing, so the placeholder
steps aside. No SSE emit, no aggregator injection, no index
collision with the LLM's streaming content: just a render slot keyed
off the parent's field.
Why not stream the cards live: whichever content index we'd choose
either blocks the LLM's text stream (`updateContent` type-mismatch at
index 0) or lands below the response after sparse compaction (index
100+). Mirroring the parent field sidesteps the aggregator entirely
and gives the user an immediate "skill is loading" signal that
naturally gives way to the real card at finalize.
Covers the gap the user flagged: pills on the user message said "I
asked for these" but nothing on the assistant side said "we're
working on it" until the stream finished. 5 new tests for the
indicator: user-msg guard, missing parent-field guard, multi-chip
render, hides-on-card-landing, orphan-parent guard.
* 🔁 fix: Indicator Visibility + Carry Manual Skills Through Regenerate/Edit
Two bugs.
Indicator never rendered: `InvokingSkillsIndicator` looked up the parent
user message via `queryClient.getQueryData([QueryKeys.messages, convoId])`,
but on a new chat the React Query cache is keyed by `"new"` (the URL
`paramId`) until the server assigns a real conversation ID — while
`message.conversationId` on the assistant message is already the server
ID. Lookup missed, `skills.length === 0`, nothing rendered. Switched
to `useChatContext().getMessages()`, which reads from the same
`paramId` the rest of the UI uses, so new-chat and existing-chat cases
both resolve to the correct message list.
Regenerate / save-and-submit dropped manual skills: the compose-time
`pendingManualSkillsByConvoId` atom is drained on the first submit,
so replaying that turn later found an empty atom and sent `manualSkills: []`.
The pills were still on the user bubble, so from the user's point of
view the model was running primed — but the backend saw nothing and
produced an unprimed response.
- Added `overrideManualSkills?: string[]` to `TOptions`. Callers with a
reference message pass its persisted `manualSkills`; `useChatFunctions.ask`
uses the override verbatim when present, otherwise falls back to the
existing drain-or-empty logic.
- `regenerate` in `useChatFunctions` passes `parentMessage.manualSkills`
— the user message being regenerated has the field persisted by the
backend, so the second turn primes the same skills as the first.
- `EditMessage.resubmitMessage` covers both edit branches:
- User-message save-and-submit: forwards the edited message's own
`manualSkills` so the new sibling turn primes identically.
- Assistant-response edit: forwards the parent user message's
`manualSkills` for the same reason.
Indicator test suite converted from `@tanstack/react-query` harness to
a jest-mocked `useChatContext().getMessages()`. 6 tests (was 5), added
a cache-miss case.
* 🧭 fix: Drive Mid-Stream Skill Chips from Submission Atom, Not Message Lookup
Message-ID-keyed lookups kept racing the stream: the user message flips
from its client-side intermediate UUID to the server-assigned ID mid-run,
conversation IDs flip from the URL `paramId="new"` to the real convo
ID on brand-new chats, and the React Query cache splits briefly between
the two. Previous attempts — direct `queryClient.getQueryData` and then
`useChatContext().getMessages()` — each missed a different window.
`TSubmission.manualSkills` is already populated at `ask()` time and the
submission atom (`store.submissionByIndex(index)`) is the single stable
anchor across the whole lifecycle: set once at submit, lives through
every SSE event, cleared when the stream ends. No ID lookups, no cache
timing.
- `InvokingSkillsIndicator` now reads `submissionByIndex(index)` via
Recoil. Shows chips when:
• the message is assistant-side,
• a submission is in flight with non-empty `manualSkills`,
• the assistant's `parentMessageId` matches
`submission.userMessage.messageId` (so chips appear only on the
bubble for the current turn, never on siblings),
• the assistant's own content doesn't yet carry a `skill`
tool_call (real card takes over from the server's post-run
`contentParts.unshift`).
- Drops the `useChatContext().getMessages()` dependency and the
`useQueryClient` dependency before that. No more lookups by
conversationId or messageId.
Test suite now mocks `useChatContext` to supply `index: 0` and seeds
the `submissionByIndex(0)` atom via Recoil initializer. 6 cases cover
user-side, no-submission history, empty `manualSkills`, multi-chip
render, hides-on-card-landing, and wrong-turn guard.
* 🌱 fix: Seed Response manualSkills in createdHandler, Indicator Becomes Pure
The mid-stream indicator kept getting wired off state I don't own: first
`queryClient.getQueryData` (raced the new-chat paramId flip), then
`useChatContext().getMessages()` (same cache, same race), then
`useRecoilValue(submissionByIndex)` (pulled every message into the
submission subscription — re-renders all indicators on any submission
change, exactly the "limit hooks in rendering" concern).
Cleanest path is the one the user pointed at: the submission owns the
data, `useSSE` / `useEventHandlers` owns the save points, so seed the
field ONTO the response message at the save site and let the indicator
be a pure prop-read.
- `createdHandler` now writes `manualSkills` onto the initial response
from `submission.manualSkills` at the moment the placeholder enters
the messages array. The field rides through the normal mutation
pipeline via spreads (`useStepHandler` response creation,
`updateContent` result returns) — no special handling needed.
- `InvokingSkillsIndicator` drops the Recoil / context / queryClient
reads. Pure function of `message`: if assistant, has `manualSkills`,
and `content` hasn't grown a `skill` tool_call yet, render chips.
Only `useLocalize` left, which was already unavoidable for the i18n
string.
- Renders decouple: no single state change (`submissionByIndex` flip,
React Query cache update) forces every indicator in the message list
to re-render anymore. Only the message whose prop changed re-runs.
Finalize story unchanged: server's `responseMessage` doesn't carry the
frontend-only `manualSkills` field, so `finalHandler`'s replacement
drops it — but by then the real `skill` tool_call is in `content`
and the indicator's content-scan hides itself anyway.
Test suite back to pure prop mocks: 7 cases covering user-guard,
no-seed, multi-chip render, skill-card-hide, non-skill-tool-call-keeps,
text-only-keeps, and missing message.
* 🪞 fix: Render Skill Indicator Inside ContentParts, Adjacent to Parts
The indicator still wasn't showing because even though MessageParts
mounted it as a sibling of ContentParts, ContentParts is a `memo`'d
component that owns the only rendering path that refreshes in lockstep
with content deltas. Mounting above it put the indicator one layer
further out — reachable, but not exercised on the same render cycle
that processes the streaming `message` prop.
Moved the indicator into ContentParts itself, rendered at the top of
both the sequential and parallel branches. Reads the `message` prop
(newly threaded through as an optional prop alongside `content`), so:
- Same render cycle as Parts — updates from the SSE pipeline flow
through the same pathway.
- Lives outside the `content.map`, so delta-driven content reshuffles
never wipe it.
- Still a pure prop-read inside the indicator itself (no Recoil,
queryClient, context hooks). The only dep is `useLocalize`.
Thread:
- `ContentPartsProps` gains `message?: TMessage`.
- `MessageParts` passes `message={message}` through, drops its own
indicator mount + import.
- `ContentParts` renders `<InvokingSkillsIndicator message={message} />`
in both the parallel-content and sequential-content branches, right
under `MemoryArtifacts` and before the empty-cursor / parts map.
Companion data flow (unchanged): `createdHandler` seeds
`initialResponse.manualSkills` from `submission.manualSkills`; the
field rides through `useStepHandler` via spreads; indicator hides on
`skill` tool_call landing in `content`.
* 🔎 refactor: Narrow Skill Components to Scalar skills Prop, Kill Memo Churn
Passing the full `message` object into presentational components busts
`React.memo` shallow comparisons every time the message reference changes
for unrelated reasons. Swap to scalar `skills?: string[]` throughout:
- `InvokingSkillsIndicator`: props-only (`skills?: string[]`); visibility
logic (user-vs-assistant, skill tool_call arrival) now lives in the
caller so this stays pure presentational.
- `ManualSkillPills`: props-only (`skills?: string[]`).
- `ContentParts`: takes `manualSkills?: string[]` scalar, computes
`showInvokingSkills` once per render from `manualSkills` + content scan
for the `skill` tool_call, then mounts the indicator with `skills=`
prop in both parallel and sequential branches.
- `MessageParts`: passes `manualSkills={message.manualSkills}` through
to `ContentParts`.
- `Container`: passes `skills={message.manualSkills}` to `ManualSkillPills`.
- Tests updated to exercise the narrowed prop surface.
* 📜 feat: Mid-Stream Skill Cards via SkillCall, Drop Custom Indicator
Instead of a separate `InvokingSkillsIndicator` chip component, render
pending skill placeholders through the existing `SkillCall` renderer —
same component the backend's finalized prime part uses. The loading
visual (`progress < 1` + empty output → pulsing "Running X") and the
completed visual ("Ran X") now come from one source of truth.
`ContentParts` computes `pendingSkillNames` from `manualSkills` minus
any `skill` tool_call already in `content` (dedupe by `args.skillName`
since the synthetic's id differs from the real one). Those names
render through a separate slot ABOVE the Parts iteration — not
prepended to the content array, which would shift React keys on
every downstream streaming text / tool part and force unmount/remount
mid-stream.
When the real prime `tool_call` lands at finalize (backend unshifts to
content[0..]), `collectExistingSkillNames` picks it up, the pending
set empties, and the real part takes over rendering in the Parts
iteration. Layout is identical either way because primes are always
at the top of content.
- `InvokingSkillsIndicator.tsx` + test deleted (no longer referenced)
- `ContentParts.tsx` renders `<SkillCall .../>` directly for pending
names, mirrors `Part.tsx`'s usage of the same component
- `createdHandler` doc comment updated to reflect the new flow
* ✂️ fix: Render Interim Skill Cards From manualSkills Only, Leave Content Untouched
Previous revision read `content` to de-dupe pending cards against real
`skill` tool_calls, so any optimistic skill part streamed from the
backend would race our placeholder off the screen mid-turn — exactly
the "getting overridden" symptom.
Now: interim `SkillCall` cards are driven purely by the response
message's `manualSkills` field. `content` is never inspected here,
so no backend delta can pull the cards down. The field is now seeded
directly onto the assistant placeholder in `useChatFunctions` (not
only in `createdHandler`) so the cards appear from the first render,
before the `created` SSE event round-trip.
Lifecycle:
- `useChatFunctions` puts `manualSkills` on the freshly-minted
`initialResponse` — cards render the instant the placeholder lands.
- `createdHandler` keeps its own re-seed (idempotent; safe) so a
regenerate / save-and-submit flow that hits that path still works.
- `useStepHandler` spread operations preserve the field through every
content update.
- `finalHandler` replaces the message with the server-backed
`responseMessage` (no `manualSkills`) — cards disappear, and the
real `skill` tool_call part in `content` takes over.
ContentParts changes:
- Drop `collectExistingSkillNames` / `parseJsonField` dedupe path.
- `renderPendingSkills` reads only `manualSkills` + `isCreatedByUser`.
- Simpler control flow — one boolean (`hasPendingSkills`) gates the
early return, one function renders.
* 🩹 fix: Codex Review Resolutions — Localization, Guards, Tests, Docs
Addresses seven findings from comprehensive code review:
Finding 1 (MAJOR) — Document sticky re-priming as intentional
- `buildSkillPrimeContentParts`: expanded doc comment explaining
synthetic `skill` tool_calls persist and get re-primed on every
subsequent turn via `extractInvokedSkillsFromPayload` (shape parity
with model-invoked skills). This matches the UX: the assistant
skill card is a visible, persistent signal that the skill is active
for the conversation. Not a bug — called out explicitly so future
maintainers don't mistake it for one.
Finding 2 (MAJOR) — Add ContentParts render tests
- New `ContentParts.test.tsx` with 7 cases covering the interim skill
card logic: assistant-only rendering, user-message suppression,
undefined-content safety, parallel+sequential branch integration,
progress<1 (pending) state. Child components mocked so the test
exercises only the branching and prop wiring ContentParts owns.
Finding 3 (MINOR) — Localize hardcoded aria-labels
- Added `com_ui_skills_manual_invoked` + `com_ui_skills_queued` keys.
- Reused existing `com_ui_remove_skill_var` for the remove-button
aria-label.
- `PendingManualSkillsChips` and `ManualSkillPills` now call
`useLocalize()`. Test mocks updated to the label-echo pattern.
Finding 4 (MINOR) — Max-length guard in `extractManualSkills`
- New `MAX_SKILL_NAME_LENGTH = 200` constant and filter. Blocks a
crafted payload like `{ manualSkills: ['a'.repeat(100000)] }` from
reaching `getSkillByName` / Mongo's query planner.
Finding 5 (NIT) — `BaseClient.js` comment contradicted itself
- Rewrote to call the filter what it is: defense-in-depth on top of
Mongoose schema validation, not a redundant second layer.
Finding 6 (NIT) — `ManualSkillPills` now wrapped in `React.memo`
- Consistent with peer components (`PendingManualSkillsChips`,
`ContentParts`). Rendered inside `Container`, which re-renders on
every content update, so the memo is a real cycle savings.
Finding 7 (NIT) — Redundant guard in `ContentParts.renderPendingSkills`
- Collapsed the duplicate null-check by computing `pendingSkills` as
a `useMemo`'d array (`[]` when not applicable), and mapping
directly. `hasPendingSkills` now derives from the array length —
one source of truth, no redundant gate inside the render function.
* 🔧 fix: Update ParallelContent to Handle Optional Content Prop
Modified the `ParallelContentRendererProps` to make the `content` prop optional, ensuring safer access within the component. Adjusted the calculation of `lastContentIdx` to handle cases where `content` may be undefined, preventing potential runtime errors. This change enhances the robustness of the component when dealing with varying message structures.
* 🎯 fix: Thread manualSkills Through ContentRender — The Real Renderer
This is why the interim skill cards never appeared across many rounds of
iteration: `ContentRender.tsx` (the memo'd renderer used by most paths,
including the agents endpoint) was calling `ContentParts` without the
`manualSkills` prop. Only `MessageParts.tsx` had it wired up — and
that's not the component that actually renders the assistant response
in production.
Two fixes:
1. Pass `manualSkills={msg.manualSkills}` to the `ContentParts` call.
2. Extend the `areContentRenderPropsEqual` memo comparator to include
`manualSkills.length`, otherwise a message update that adds the
field (seeded by `useChatFunctions` on the initialResponse) would
be bailed out by the memo and never re-render.
Verified the two ContentParts call sites are now consistent; Container
usages for `ManualSkillPills` on the user side were already correct.
* 🧹 polish: Address Audit Follow-Up (F1/F3/F6)
F1 — Clarify sticky re-priming opt-out path.
The previous comment said "regenerate without the pick" as one
opt-out, but `useChatFunctions.regenerate` forwards the original
picks via `overrideManualSkills`, so regeneration alone keeps the
skill sticky. Updated to: edit the originating message to remove
the pills and resubmit, or start a new conversation.
F3 — Add DOM-order assertions to the parallel + sequential tests.
The two "alongside" tests verified both elements existed but
didn't pin the ordering contract. Both now use
`compareDocumentPosition` to assert the pending SkillCall
precedes the real content, matching the backend semantic
(`contentParts.unshift(...primeParts)` puts primes at the top).
F6 — Fix package import order in PendingManualSkillsChips.
`recoil` (58 chars) was listed before `lucide-react` (45 chars)
which violates the "shortest to longest after react" rule in
AGENTS.md. Swapped order; no behavior change.
F2 / F4 / F5 from the audit were confirmed as non-issues
(React-safe empty map, cosmetic test-mock artifact, accepted
memo tradeoff) and require no change.
* ✨ feat: Dedicated PendingSkillCall + Running→Ran Transition on Real Content
UX polish on the interim skill card now that it's actually rendering:
1. New `PendingSkillCall` component (mirrors `SkillCall` visually but
drops the expand affordance). `SkillCall`'s underlying `ProgressText`
always renders a chevron + clickable button when any input is
present, which on a card with empty output points at nothing —
misleading cursor:pointer and a no-op toggle. The pending variant
has only the icon + label, no button wrapper, no chevron.
2. "Running X" → "Ran X" transition when real content lands.
`ContentParts` computes `hasRealContent` (any non-text part, or a
text part with non-empty content — placeholder empty-text parts
don't count) and passes `loaded={hasRealContent}` to
`PendingSkillCall`. Matches what users see for model-invoked skills
as they finish priming: pulsing shimmer → static icon.
3. Cleanup:
- Dropped direct `SkillCall` import from `ContentParts` (replaced
by `PendingSkillCall`). `SkillCall` is still used by `Part` for
real `skill` tool_call content parts — no behavior change there.
- Removed the now-redundant explicit `manualSkills` assignment
in `createdHandler`. `useChatFunctions` seeds the field on
`initialResponse` at construction, so the `...submission.initialResponse`
spread already carries it through — the re-assignment was
defensive belt-and-suspenders doing the same work twice. Comment
rewritten to describe the actual lifecycle.
Tests updated to the new component (12/12 pass): two new cases pin
the loaded-state transition (unloaded when content has no real parts,
flips to loaded once a non-empty text part lands).
* feat: compose per-agent scope and per-user active-state filters in $ popover
Stack two runtime-truth filters on top of the existing `isUserInvocable`
check so the `$` skill popover matches what will actually be available at
turn time:
- Per-agent skill scope from `agent.skills` (resolved via useChatContext
+ useAgentsMapContext), mirroring backend `scopeSkillIds` semantics:
`undefined`/`null` → no scope, `[]` → empty, non-empty → intersection.
- Per-user ownership-aware active state via `useSkillActiveState().isActive`.
The filters are composed in a pure, exported `filterSkillsForPopover`
helper so the agent-scope ∩ active ∩ invocable matrix can be unit-tested
without rendering the component. Short-circuits on cheapest check first
(agent scope → active → invocation mode).
Backend still enforces both filters at runtime; this PR is a UX mirror so
users do not see popover entries that would be filtered out by the time
the LLM turn begins.
* refactor: thread agentId into SkillsCommand as a prop
Drop the direct `useChatContext()` call inside SkillsCommand in favor of
receiving `agentId` from ChatForm. The parent already subscribes to the
conversation via its single useChatContext call, and SkillsCommand is
wrapped in React.memo — threading the id as a prop means the popover only
re-renders when agent_id actually changes instead of on every unrelated
conversation-shape mutation. Mirrors the pattern AttachFileChat already
uses for `conversation` / `agentId`.
No behavior change; filter semantics and test coverage are identical.
* fix: fail closed when agent skill scope cannot be resolved
Previously, if `conversation.agent_id` was set but the agents map had no
entry for it (hydration pending, query failure, or missing VIEW access),
the popover treated the scope as `undefined` and showed the full ACL
catalog. That leaks options the backend will reject at turn time, the
opposite of what this phase is meant to do.
Distinguish the unresolved cases ("map undefined" and "agent not in map")
from the intentionally-unconfigured case ("agent exists, no `skills`
field") and return `[]` for the former, preserving the backend semantics
of `scopeSkillIds` only for the latter. Adds two tests covering both
fail-closed branches.
* fix: surface agent.skills in list projection and treat ephemeral ids as unscoped
Two holes flagged on the earlier fail-closed commit:
1. The list-agents projection in `getListAgentsByAccess` omitted the
`skills` field, so `agentsMap[agentId].skills` was always undefined
and the popover fell back to the full ACL catalog for every scoped
agent — the opposite of this phase's intent. Add `skills: 1` to the
projection and lock it in with a new test.
2. Conversations can carry ephemeral agent ids (e.g. `ephemeral` after
switching off the agents endpoint) that intentionally don't live in
the agents map. The prior fail-closed branch blanked the popover in
those cases. Treat anything that doesn't start with `agent_` as
unscoped via the existing `isEphemeralAgent` helper so the popover
shows the full ACL-visible catalog, matching how no-agent convos
already behave.
Frontend: 18 tests pass (adds one ephemeral-id case).
Backend: 10 getListAgentsByAccess tests pass (adds one projection case).
* refactor: pass through hydration race, only fail closed when map is authoritative
Split the two "cannot resolve scope" cases that were previously collapsed
onto the same fail-closed branch:
- `agentsMap === undefined` means the agents list query has not settled
yet. Return `undefined` (full catalog). The map typically hydrates well
before the first `$` open, and the backend still scopes at turn time —
blanking the popover during a sub-second race produces worse UX with
no security benefit.
- `agentsMap` is populated but the agent is absent means the agent was
deleted or the user's VIEW access was revoked mid-session. That is
authoritative missing state, so keep the fail-closed behavior — the
full catalog would be misleading.
Updates the associated test case to assert the hydration-race path now
shows the catalog, and rewrites the in-code comment to distinguish the
two branches.
* refactor: drop `as string` in agent scope memo by tightening the guard
`isEphemeralAgent` returns true for null/undefined so the original code
was runtime-safe, but its signature returns plain `boolean` rather than
a type predicate, so TypeScript never narrowed `agentId` to `string` and
the subsequent map lookup required an `as string` assertion. Split the
guard into `!agentId || isEphemeralAgent(agentId)` so the narrowing falls
out naturally and the assertion can be removed.
No behavior change; 18 tests pass.
* feat: per-user skill active/inactive toggle with ownership-aware defaults
- Add `skillStates` map (Record<string, boolean>) to user schema for
per-user active/inactive overrides on skills
- Add `defaultActiveOnShare` to interface.skills config (default: false)
so admins can control whether shared skills auto-activate
- Add GET/POST /api/user/settings/skills/active endpoints with validation
- Add React Query hooks with optimistic mutations for skill states
- Add useSkillActiveState hook with ownership-aware resolution:
owned skills default active, shared skills default inactive
- Add toggle switch UI to SkillListItem and SkillDetail components
- Filter inactive skills in injectSkillCatalog before agent injection
- Add localization keys for active/inactive labels
* fix: use Record instead of Map for IUser.skillStates
Mongoose .lean() flattens Map to a plain object, causing type
incompatibility with IUser in methods that return lean documents.
* fix: address review findings for skill active states
- Fail-closed when userId is absent: filter rejects all shared skills
instead of passing them through unfiltered (Codex P1)
- Validate Mongoose Map key characters (reject . and $) in controller
to return 400 instead of a 500 from schema validation (Codex P2)
- Block toggle while initial skill states query is loading to prevent
overwriting server-side overrides with an empty snapshot (Codex P2)
- Extract shared SkillToggle component, eliminating duplicate toggle
markup in SkillListItem and SkillDetail (Finding #3)
- Move skill state query/mutation hooks from Favorites.ts to
Skills/queries.ts per feature-directory convention (Finding #4)
- Fix hardcoded English aria-label in SkillListItem by passing the
localized string from the parent SkillList (Finding #5)
- Fix inline arrow in SkillList render loop: pass stable callback
reference so SkillListItem memo() is not invalidated (Finding #1)
- Extract toRecord() helper in controller to DRY the Map-to-Object
conversion (Finding #6)
- Remove Promise.resolve wrapping synchronous config read (Finding #8)
- Remove unused TUpdateSkillStatesRequest type (Finding #12)
* fix: forward tabIndex on SkillToggle to preserve list keyboard nav
The original inline toggle had tabIndex={-1} so the row itself
remained the sole tab target. The extraction into SkillToggle
dropped this prop, making every list toggle a tab stop. Add an
optional tabIndex prop and pass -1 from SkillListItem.
* fix: plumb skillStates to all agent entry points, isolate toggle keydown
- Add skillStates/defaultActiveOnShare loading to openai.js and
responses.js controllers so shared-skill activation is respected
across all agent entry points, not just initialize.js (Codex P1)
- Stop keydown propagation on SkillToggle so Enter/Space does not
bubble to the parent row's navigation handler (Codex P2)
* fix: paginate catalog fetch and serialize toggle writes
- Paginate listSkillsByAccess (up to 10 pages of 100) until the active
catalog quota is filled, so inactive shared skills in recent positions
do not starve active owned skills past the first page (Codex P1)
- Extend listSkillsByAccess interface with cursor/has_more/after for
catalog pagination
- Serialize skill-state writes via a ref queue: one in-flight request
at a time, with the latest desired state sent when the previous one
settles. Prevents last-response-wins races where an older request
overwrites newer toggles (Codex P2)
* fix: share write queue across hook instances, block toggle on fetch error
- Move the write queue from a per-instance useRef to a module-scoped
object so every mount of useSkillActiveState (SkillList, SkillDetail,
etc.) serializes against the same in-flight slot. Prior per-instance
queues allowed two components to race full-map POSTs (Codex P1)
- Extend the toggle guard beyond isLoading: also block when isError is
true or data is undefined. Prevents a failed GET from seeding a
toggle with an empty baseline that would wipe server-side overrides
on the next successful POST (Codex P1)
* fix: stale closure, orphan cleanup, and cap-error UX
- Read toggle baseline from React Query cache via queryClient.getQueryData
instead of the captured skillStates closure. The closure can be stale
between onMutate's setQueryData and the next render, so rapid successive
toggles would build on old state and drop earlier changes (Codex P1)
- Surface the MAX_SKILL_STATES_EXCEEDED error code with a specific toast
key (com_ui_skill_states_limit) so users understand the 200-cap rather
than seeing a generic error
- Prune orphaned entries (skillIds whose Skill doc no longer exists) on
both GET and POST in SkillStatesController. Self-heals over time
without needing cascade-delete hooks or a migration job. Uses one
indexed Skill._id query per request
* test: pin skill active-state precedence with unit tests
Extract the active-state resolution logic from a closure inside
injectSkillCatalog into an exported resolveSkillActive helper, then
cover every branch of the precedence matrix:
- Fails closed when userId is absent (even with defaultActiveOnShare=true)
- Explicit override wins over ownership and config (both true and false)
- Owned skills default to active when no override is set
- Shared skills default to defaultActiveOnShare value
- Undefined skillStates behaves identically to an empty object
- defaultActiveOnShare defaults to false when omitted
- Owned skills ignore defaultActiveOnShare entirely
Closes Finding #2 from the pre-rebase comprehensive review. Mirrors
the existing scopeSkillIds test style; injectSkillCatalog now calls
resolveSkillActive instead of inlining the closure.
* refactor: limit skill active toggle to detail header, drop label
- Remove the per-row toggle from SkillListItem and the active-state
plumbing (hook call, isSkillEnabled/onToggleEnabled/toggleAriaLabel
props) from SkillList. The detail view is now the single place to
change a skill's active state
- Drop dim/muted styling for inactive skills in the sidebar: without
a control there, the visual indication has nowhere to land
- Resize SkillToggle to match neighbor buttons: outer h-9 container,
h-6 w-11 track with size-5 knob, no label span. The 'Active' /
'Inactive' text that accompanied the detail-view toggle is removed
- Remove the now-unused label prop and tabIndex prop (the tabIndex
existed only for the list-row context) from SkillToggle. Drop the
onKeyDown stopPropagation for the same reason
- Remove now-orphaned com_ui_skill_active / com_ui_skill_inactive
translation keys
* style: shrink SkillToggle track to h-5 w-9 with size-4 knob
Container stays at h-9 to match neighbor button heights. The toggle
track itself drops from h-6 w-11 to h-5 w-9, with a size-4 knob
travelling 1.125rem on activation. Visually lighter inside the row.
* fix: remove redundant skillStates entries that match the resolved default
When a toggle lands on the ownership/config default, delete the key
from the map instead of persisting `{id: defaultValue}`. Without this,
a user toggling a skill off and back on would leave `{id: true}` for
an owned skill (whose default is already true), silently consuming a
slot against the 200-entry cap. Repeated round-trip toggles could
exhaust the quota with zero meaningful overrides (Codex P2).
Preserves the exceptions-list invariant that the runtime-resolution
design depends on.
* fix: prune before enforcing skill-state cap; reject non-ObjectId keys
Reorder the update controller so pruneOrphans runs before the 200-cap
check. Without this, a user near the cap with some orphaned entries
(skills deleted since their last GET) could send a payload that would
pass after pruning but gets rejected by the raw-size check first.
Add a sanity cap on raw payload size (2 * MAX_SKILL_STATES) so abusive
inputs do not reach the DB query, and enforce the real cap on the
pruned result instead.
Harden pruneOrphans: the earlier early-return path could pass
non-ObjectId keys through unchanged. Now only valid ObjectIds are
returned, and the Skill-model-unavailable fallback filters by format.
Also add isValidObjectIdString validation at the input boundary so
malformed (but otherwise non-Mongo-unsafe) keys never reach persistence
(Codex P2 x2).
* fix: enforce active filter at execute time, prune revoked shares, scope queue per user
P1: injectSkillCatalog now returns activeSkillIds (the filtered set
that appears in the catalog). initializeAgent uses that set as the
stored accessibleSkillIds on the initialized agent, so getSkillByName
at runtime cannot resolve a deactivated skill — even if the LLM
hallucinates a name or the user invokes by direct-invocation shorthand.
Previously the executor authorized against the full ACL set, bypassing
the active-state guarantee (Codex P1).
P2: pruneOrphans now checks user access via findAccessibleResources
in addition to skill existence. When a share is revoked, the user's
skillStates entry for that skill had no cleanup path and silently
consumed the 200-cap. Self-heals on both GET and POST. One extra ACL
query per settings read/write; scoped to a single user so no N-user
amplification (Codex P2).
P2: the write queue moves from a single module-scoped object to a Map
keyed by userId. Logout/login in the same tab can no longer flush the
previous user's pending snapshot under the new session's auth. Each
userId gets its own pending/inFlight slot; the in-flight request
retains its original auth via the cookie already attached when sent,
so the race window closes (Codex P2).
* refactor: extract skillStates helpers to packages/api; add tests; polish
Address the remaining valid findings from the comprehensive review:
- Extract toRecord, loadSkillStates, validateSkillStatesPayload, and
pruneOrphanSkillStates into packages/api/src/skills/skillStates.ts
as TypeScript. The controller in /api shrinks to a ~90-line thin
wrapper that builds live dependency adapters for Mongoose + the
permission service (Review #2 DRY, #3 workspace boundary)
- Replace the triplicated 12-line skillStates loading block in
initialize.js, openai.js, and responses.js with a single call to
loadSkillStates from @librechat/api. One helper, three sites
- Swap console.error for the project logger in the controller
(Review #7)
- Remove the redundant INVALID_KEY_PATTERN regex: a valid ObjectId
cannot contain . or $, so isValidObjectIdString already covers it
(Review #11)
- Parameterize the 200-cap error toast with {{0}} interpolation
driven by the error response's `limit` field, so future changes to
MAX_SKILL_STATES update the UI message automatically (Review #12)
- Add 24 unit tests for the new skillStates helpers (toRecord,
resolveDefaultActiveOnShare, loadSkillStates, validateSkillStates-
Payload, pruneOrphanSkillStates) covering success paths, malformed
input, cap boundaries, and parallel-query behavior (Review #4)
- Add 10 tests for injectSkillCatalog pagination covering empty
accessible set, missing listSkillsByAccess, single-page filter,
owned-vs-shared defaults, explicit-override precedence, multi-page
collection, MAX_CATALOG_PAGES safety cap, early termination on
has_more=false, additional_instructions injection, and fail-closed
without userId (Review #5)
Total test count: 60 (was 26 on this surface).
* fix: rename skillStates ValidationError to avoid barrel-export collision
packages/api/src/types/error.ts already exports a ValidationError
(MongooseError extension). Re-exporting a different shape from
skills/skillStates.ts through the skills barrel caused TS2308 in CI
because the root index re-exports both. Rename to
SkillStatesValidationError to keep the exports disjoint.
* refactor: tighten tests and absorb caller guard into loadSkillStates
Address the followup review findings:
- Add optional `accessibleSkillIds` param to loadSkillStates so the
helper short-circuits to defaults when no skills are accessible.
All three controllers drop the residual 7-line conditional wrapper
in favor of a single destructured call (Review #2)
- Remove the unreachable `typeof key !== 'string'` check from
validateSkillStatesPayload: Object.entries always yields string
keys per the JS spec (Review #3)
- Replace the two `as unknown as` agent casts in the injectSkillCatalog
tests with a `makeAgent()` factory typed directly as the function's
parameter shape (Review #4)
- Tighten the MAX_CATALOG_PAGES assertion from `toBeLessThanOrEqual(11)`
to `toHaveBeenCalledTimes(10)` — the loop deterministically makes
exactly 10 page fetches before hitting the cap (Review #1)
- Rewrite the parallel-execution test for pruneOrphanSkillStates using
deferred promises instead of microtask-order assertions. The test
now inspects `toHaveBeenCalledTimes(1)` on both mocks after a single
Promise.resolve() yield, pinning Promise.all usage without relying
on push-order into a shared array (Review #5)
- Evict stale writeQueue entries on user change via a module-scoped
`lastSeenUserId` sentinel. When a different user's toggle is the
first one after a logout/login, the previous user's queue entry is
deleted. Keeps the Map bounded without adding hook-instance effect
cleanup (Review #6)
* fix(test): mock loadSkillStates in openai and responses controller specs
The prior refactor replaced the inline 12-line skillStates loading
block with a call to loadSkillStates from @librechat/api. Both
controller spec files mock @librechat/api as a flat object, so any
new named import from that package is undefined in the test env.
Calling `await loadSkillStates(...)` threw before recordCollectedUsage
ran, surfacing as "undefined is not iterable" on the test's array
destructure of `mockRecordCollectedUsage.mock.calls[0]`.
Add the missing mock to both spec files alongside the existing
scopeSkillIds stub.
* fix: abandon stale skillStates write queues on user switch
Close the cross-session leak window where an in-flight flush loop
still holds a reference to a previous user's queue: it could fire its
next mutateAsync under the new session's auth cookies and persist
the stale snapshot to the new user's document (Codex P1).
Add an `abandoned` flag on `WriteQueue`. Three mechanisms cooperate:
- `getWriteQueue` marks every non-active queue abandoned when the
user differs from the last-seen identity (pre-existing eviction
site, now more aggressive).
- A `useEffect` on `userId` calls the same abandonment pass on every
render with a new active identity, covering the window between
logout/login and the new user's first toggle (when `getWriteQueue`
would otherwise not fire).
- The flush loop checks `!queue.abandoned` in its while condition so
the second and later iterations exit without firing another
`mutateAsync` after the session changes.
The first iteration's in-flight request (already dispatched under the
original user's cookies) still runs to completion or failure on its
own — only the subsequent iterations, which are the dangerous ones,
are blocked.
* feat: per-agent skill selection in builder and runtime scoping
Wire skills persistence on the Agent model and enable the skills
section in the agents builder panel. At runtime, scope the skill
catalog to only the skills configured on each agent (intersected
with user ACL). When no skills are configured, the full user catalog
is used as the default. The ephemeral chat toggle overrides per-agent
scoping to provide the full catalog.
* fix: add scopeSkillIds to @librechat/api mock in responses unit test
The test mocks @librechat/api but was missing the newly imported
scopeSkillIds, causing createResponse to throw before reaching the
assertions. Added a passthrough mock that returns the input array.
* fix: scope primeInvokedSkills by agent's configured skills
primeInvokedSkills was receiving the full unscoped accessibleSkillIds,
bypassing the per-agent skill scoping applied to initializeAgent. This
allowed previously invoked skills from message history to be resolved
and primed even when excluded from the agent's configured skill set.
Apply the same scopeSkillIds filtering to match the initializeAgent
calls, so skill resolution is consistent across catalog injection
and history priming.
* fix: preserve agent skills through form reset and union prime scope
Two related bugs in the per-agent skill selection flow:
1. resetAgentForm dropped the persisted skills array because the generic
fall-through at the end of the loop excludes object/array values.
Combined with composeAgentUpdatePayload always emitting skills, this
caused any save of a previously-configured agent to silently overwrite
skills with an empty array. Add an explicit case for skills mirroring
the agent_ids handling.
2. primeInvokedSkills processes the full conversation payload, including
prior handoff-agent invocations. Scoping it to only primaryAgent.skills
meant a skill invoked by a handoff agent in a prior turn could not be
resolved when the current primary agent had a different scope, leaving
message history reconstruction incomplete. Union the per-agent scoped
accessibleSkillIds across primary plus all loaded handoff agents so
any skill any active agent could invoke is resolvable from history.
* fix: mark inline skill removals as dirty
The inline X button on the skills list called setValue without
shouldDirty: true, so removing a skill via this control did not
mark the skills field as dirty in react-hook-form state. When a
user removed a skill with the X button and also staged an avatar
upload in the same save, isAvatarUploadOnlyDirty returned true and
onSubmit short-circuited to avatar-only upload, silently dropping
the PATCH that would persist the skill removal.
The dialog path (SkillSelectDialog) already passes shouldDirty: true
on add/remove; this aligns the inline control with that behavior.
* fix: restore full ACL scope for primeInvokedSkills history reconstruction
Reverting the earlier scoping of primeInvokedSkills to the active-agent
union. That change conflated runtime invocation scoping (which correctly
gates what the model can call now) with history reconstruction (which
restores bodies the model already saw in prior turns).
Per-agent scoping still applies at:
- Catalog injection (injectSkillCatalog via initializeAgent)
- Runtime invocation (handleSkillToolCall via enrichWithSkillConfigurable,
using each agent's scoped accessibleSkillIds in agentToolContexts)
History priming is a read of past context, not a grant of new capability.
Scoping it causes historical skill bodies to vanish from formatAgentMessages
when an agent's skills list is edited mid-conversation or when the ephemeral
toggle flips, which breaks message reconstruction and drops code-env file
continuity for /mnt/data/{skillName}/ references. The user's ACL-accessible
set is the correct and sufficient gate for history reconstruction.
* fix: close openai.js skill gap and pin undefined vs [] semantics
Three related gaps surfaced in review:
1. api/server/controllers/agents/openai.js was a third skill resolution
site alongside responses.js and initialize.js, but still used the old
activation gate (required ephemeralAgent.skills === true) and never
passed accessibleSkillIds through scopeSkillIds. Per-agent scoping
silently did not apply on this route. Mirror the same pattern used
in responses.js so all three routes behave identically.
2. scopeSkillIds previously collapsed undefined and [] into the same
"full catalog" fallback, making it impossible for a user to express
"this agent has no skills." Tighten the semantics before any data
is written under the old behavior:
- undefined / null = not configured, full catalog
- [] = explicitly none, returns []
- non-empty = intersection with ACL-accessible set
Update defaultAgentFormValues.skills from [] to undefined so a brand
new agent whose skills UI was never touched does not accidentally
persist "explicit none" on first save (removeNullishValues strips
undefined from the payload server side).
3. Add direct unit tests for scopeSkillIds covering all five cases
(undefined, null, empty, disjoint, overlap, exact match, empty
accessible set). 16 tests total in skills.test.ts pass.
* fix: add scopeSkillIds to @librechat/api mock in openai unit test
Same pattern as the earlier responses.unit.spec.js fix: the test mocks
@librechat/api with an explicit object, so each newly imported symbol
must be added to the mock. Without scopeSkillIds, OpenAIChatCompletion
controller throws on destructuring before reaching recordCollectedUsage,
causing the token usage assertions to fail.
* feat: add $ command popover for manual skill invocation
Adds a new `$` command trigger in the chat textarea that opens a
searchable popover listing user-invocable skills. Selecting a skill
inserts `$skill-name` into the message and enables the skills badge
on the ephemeral agent. Follows the same patterns as `@` mentions
and `/` prompts.
* test: update useHandleKeyUp tests for $ skill command
Add showSkillsPopoverFamily, dollarCommand, and SKILLS permission
to the store/recoil/access mocks. Include test cases for the $
trigger, toggle gating, and permission gating.
* fix: address review findings in SkillsCommand
- Exclude auto-mode skills from popover (isUserInvocable now
returns false for InvocationMode.auto)
- Rewrite JSDoc to match the corrected filter logic
- Guard ArrowUp/ArrowDown against NaN when matches is empty
- Replace useRecoilState with useSetRecoilState for ephemeralAgent
to avoid subscribing to unrelated agent state changes
- Move skills guard into setter callback and drop ephemeralAgent
from handleSelect dependency array
- Add e.preventDefault() for Tab key alongside Enter
- Render error state (com_ui_skills_load_error) on query failure
- Render empty state (com_ui_skills_empty) when no matches
- Hoist ScrollText icon to module-level constant
- Export isUserInvocable for testability
* fix: address follow-up review findings
- Differentiate empty-catalog ("No skills yet") from no-match
search ("No skills found") using existing com_ui_no_skills_found
- Clamp activeIndex when matches shrink from search filtering to
prevent silent Enter/Tab failures
- Add early return after Escape handler to skip redundant checks
- Reorder package imports shortest-to-longest after react
- Add fast-typing test case for $ command ("$sk" at position 3)
* fix: gate $ command on assistants endpoint, fix Tab on empty matches
- Block $ popover on assistants/azureAssistants endpoints where
ephemeralAgent is not sent in the submission payload
- Allow Tab to close the popover when matches is empty instead of
trapping keyboard focus
- Add endpoint gating tests for $ on both assistants endpoints
* fix: reset skills popover on assistants switch + paginate skills query
- Close $ skills popover when endpoint switches to assistants or
azureAssistants, mirroring the existing + command reset
- Switch SkillsCommand from a single-page list query to the
cursor-paginated useSkillsInfiniteQuery and auto-fetch all pages
so client-side search covers the full catalog instead of only
the first 100 entries
- Show the spinner during background page fetches and suppress the
empty-state copy until paging completes
- Add test for popover reset on endpoint switch
* fix: prevent currency hijack and form submit on $ command
- Reject the $ trigger when fast-typed text after $ does not start
with a lowercase letter, so currency input like $100 or $5.99
no longer opens the skills popover and clears the textarea
- preventDefault() on Enter when the popover has no matches so
the surrounding chat form does not submit when the user dismisses
the popover via Enter
- Add tests for $100 and $5.99 currency inputs
* fix: defer $ popover to second keystroke and stop pagination on errors
- Defer opening the skills popover until a letter follows the $
character. Bare $ no longer triggers the popover, so starting a
message with $100, $5.99, or $EUR is fully preserved (the
textarea is not cleared by useInitPopoverInput on the first
keystroke). $a, $skill, $my-skill still open as expected.
- Add a sticky paginationBlockedRef circuit breaker on the auto
fetchNextPage effect so a transient page request error cannot
spin into an unbounded retry loop when isError flips back to
false on the next attempt.
- Update tests: bare $ no longer triggers, $a does, currency cases
remain blocked.
* feat: scaffold structured manual-skill channel for follow-up PR
Add a per-conversation pendingManualSkillsByConvoId atom family
that SkillsCommand appends to on selection. This is the writer
half of the structured channel that will let a follow-up PR
deterministically prime SKILL.md as a meta user message before
the LLM turn (mirroring Claude Code's `/skill` invocation), so
the backend never has to regex-parse `$name` out of user text.
The submit pipeline does not yet read this atom; the textual
`$skill-name ` insertion remains the authoritative signal until
the follow-up wires the read + reset on submit. Reset is already
wired into useClearStates so the atom does not leak across
conversation switches.
* test: cover SkillsCommand selection-flow contract
Add a component test that locks in the contract the follow-up
manualSkills PR has to honor when a user picks a skill in the $
popover:
- Pushes the skill name onto pendingManualSkillsByConvoId (the
per-conversation structured channel), with dedup
- Flips ephemeralAgent.skills to true via the callback-form setter
- Inserts $skill-name into the textarea as cosmetic confirmation
- Closes the popover via setShowSkillsPopover(false)
- Renders nothing when the popover atom is false
Mocks recoil setters and the skills query so the test exercises
the real component logic without spinning up the full provider
stack.
* revert: drop $ defer-until-letter guard for sibling consistency
Bring $ in line with @, /, and +: open the popover on the bare
trigger character. The earlier defer guard kept currency input
like \$100 from clobbering the textarea, but it broke the natural
"type \$ to browse skills" UX and was inconsistent with the other
trigger chars, none of which gate on the follow-up character
(/path/to/file, @username, +1 all clear the textarea the same way).
The currency hijack remains recoverable via Escape.
Drop the corresponding paste-protection tests for \$100 and \$5.99,
restore the bare-\$ trigger test.
* feat: Custom UI renderers for skill, read_file, and bash_tool
Add specialized tool call components for the three skill tools,
replacing the generic ToolCall fallback with contextual UI.
* fix: Address review findings for skill tool UI renderers
- Fix Codex P2: read skillName (camelCase) matching agent pipeline
- Fix Codex P2: remove error regex from ReadFileCall to avoid false
positives on normal file content containing "Error:" tokens
- Extract useToolCallState hook to eliminate ~60% boilerplate
duplication across SkillCall, ReadFileCall, and BashCall
- Extract parseJsonField utility with consistent escaped-char-aware
regex fallback, shared by all three components
- Gate SkillCall bordered card on hasOutput to prevent empty card
when expanded before output arrives
- Skip highlightAuto for plaintext lang to avoid expensive
auto-detection on files with unknown extensions
- Expand LANG_MAP with php, cs, kt, swift, scss, less, lua, r;
add FILENAME_MAP for Makefile and Dockerfile
- Export langFromPath for testability
- Add unit tests for parseJsonField, langFromPath, and ToolIcon
skill type branches
* refactor: Redesign BashCall as minimal terminal widget
Replace the ExecuteCode-clone pattern with a purpose-built terminal
UI: $ prompt prefix, dark background command zone, icon-only copy
button, and raw monospace output. Drops useLazyHighlight,
CodeWindowHeader, Stdout, and the "Output" label in favor of a
cleaner two-zone layout that feels native to the terminal.
* fix: parseJsonField unescape ordering and ReadFileCall empty card
Replace the sequential .replace() chain in parseJsonField's regex
fallback with a single-pass /\(.)/g replacement. The old chain
processed \n before \, so \n (JSON-escaped literal backslash + n)
was incorrectly decoded as a newline instead of \n.
Gate ReadFileCall's bordered card on hasOutput (matching SkillCall's
pattern) so the card does not render as an empty rounded box during
streaming before output arrives.
Add regression tests for \n decoding and unknown escape sequences.
* fix: Followup review fixes
- Refactor ExecuteCode to use shared useToolCallState hook,
eliminating the last copy of the inline state machine
- Escape regex metacharacters in parseJsonField to prevent
injection from field names containing ., +, (, etc.
- Fix contradictory test description in langFromPath tests
* fix: Surface tool failure state in skill tool renderers
Add error detection to useToolCallState via the shared isError
check so tool calls that complete with an error prefix show a
"failed" suffix instead of a success label. Prevents misleading
users when read_file, skill, or bash_tool returns an error
(e.g. file not found, skill not accessible). Matches the error
handling pattern already used by the generic ToolCall component.
* feat: Add bash syntax highlighting to BashCall command zone
Reuse the shared useLazyHighlight singleton (already loaded by
ReadFileCall and ExecuteCode) to highlight the command with bash
grammar. Falls back to plain text while lowlight is loading.
* fix: Align BashCall scrollbar to span full card width
Move max-h/overflow-auto from the inner pre to the outer container
so the scrollbar spans the full width like the output zone. Float
the copy button with sticky positioning so it stays visible while
scrolling long commands.
* feat: Use GNU Bash icon for bash_tool progress header and ToolIcon
Replace the generic SquareTerminal lucide icon with the GNU Bash
logo (already in the project via LangIcon/langIconPaths) for
both the BashCall progress header and the ToolIcon stacked icon
mapping.
* fix: Render raw content while highlighter loads, preserve command text on copy
- ReadFileCall: fall back to raw output when useLazyHighlight
returns null, preventing a blank code panel on first render
before lowlight finishes its dynamic import
- BashCall: drop .trim() from the copy handler so the clipboard
receives exactly what's displayed (WYSIWYG copy)
* fix: Alphabetize new translation keys within en/translation.json
Relocate read_file, skill_finished, and skill_running into their
correct alphabetical positions within the overall key list.
* fix: Surface error state in ExecuteCode, fix BashCall import order
- ExecuteCode now uses hasError from useToolCallState to show
the "failed" suffix on failed code executions, matching the
three new renderers
- Reorder BashCall local imports to longest-to-shortest per
project style
* feat: Skill runtime integration — catalog injection, tool registration, execute handler
Wires the @librechat/agents SkillTool primitive into LibreChat's agent runtime:
**Enums:**
- Add `skills` to AgentCapabilities + defaultAgentCapabilities
**Data layer:**
- Add `getSkillByName(name, accessibleIds)` — compound query that
combines name lookup + ACL check in one findOne
**Agent initialization (packages/api/src/agents/initialize.ts):**
- Accept `accessibleSkillIds` param and `listSkillsByAccess` db method
- Query accessible skills, format catalog via `formatSkillCatalog()`,
append to `additional_instructions` (appears in agent system prompt)
- Register `SkillToolDefinition` + `createSkillTool()` when catalog
is non-empty (tool appears in model's tool list)
- Store `accessibleSkillIds` and `skillCount` on InitializedAgent
**Execute handler (packages/api/src/agents/handlers.ts):**
- Add `getSkillByName` to `ToolExecuteOptions`
- `handleSkillToolCall()` intercepts `Constants.SKILL_TOOL`:
extracts skillName, loads body from DB with ACL check,
substitutes $ARGUMENTS, returns ToolExecuteResult with
injectedMessages (skill body as isMeta user message)
**Caller wiring:**
- initialize.js: query skill IDs via findAccessibleResources,
pass to initializeAgent + store on agentToolContexts,
add getSkillByName to toolExecuteOptions,
pass accessibleSkillIds through loadTools configurable
- openai.js + responses.js: same pattern for their flows
Requires @librechat/agents >= 3.1.65 (PR #91 exports).
* feat: Skills toggle in tools menu + backend capability gating
Frontend:
- Add skills?: boolean to TEphemeralAgent type
- Add LAST_SKILLS_TOGGLE_ to LocalStorageKeys for persistence
- Add skillsEnabled to useAgentCapabilities hook
- Add skills useToolToggle to BadgeRowContext with localStorage init
- New Skills.tsx badge component (Scroll icon, cyan theme,
permission-gated via PermissionTypes.SKILLS)
- Add skills entry to ToolsDropdown with toggle + pin
- Render Skills badge in BadgeRow ephemeral section
Backend:
- Extract injectSkillCatalog() into packages/api/src/agents/skills.ts
(reduces initializeAgent module size, reusable helper)
- initializeAgent delegates to helper instead of inline block
- Capability-gate the findAccessibleResources query:
- Agents endpoint: checks AgentCapabilities.skills in admin config
- OpenAI/Responses controllers: checks ephemeralAgent.skills toggle
- ACL query runs once per run, result shared across all agents
* refactor: remove createSkillTool() instance from injectSkillCatalog
SkillTool is event-driven only. The tool definition in toolDefinitions
is sufficient for the LLM to see the tool schema. No tool instance is
needed since the host handler intercepts via ON_TOOL_EXECUTE before
tool.invoke() is ever called.
Removes tools from InjectSkillCatalogParams/Result, drops the
createSkillTool import.
* feat: skill file priming, bash tool, and invoked skills state
Multi-file skill support:
- New primeSkillFiles() helper (packages/api/src/agents/skillFiles.ts)
uploads skill files + SKILL.md body to code execution environment
- handleSkillToolCall primes files on invocation when skill.fileCount > 0,
returns session info as artifact so ToolNode stores the session
- Skill-primed files available to subsequent bash/code tool calls
Bash tool auto-registration:
- BashExecutionToolDefinition added alongside SkillToolDefinition when
skills are enabled, giving the model a bash tool for running scripts
Conversation state:
- Add invokedSkillIds field to conversation schema (Mongoose + Zod)
- handleSkillToolCall updates conversation with $addToSet on success
- Enables re-priming skill files on subsequent runs (future)
Dependency wiring:
- Pass listSkillFiles, getStrategyFunctions, uploadCodeEnvFile,
updateConversation through ToolExecuteOptions
- Pass req and codeApiKey through mergedConfigurable
- All three controller entry points wired (initialize.js, openai.js,
responses.js)
* fix: load bash_tool instance in loadToolsForExecution, remove file listing
- Add createBashExecutionTool to loadToolsForExecution alongside PTC/ToolSearch
pattern: loads CODE_API_KEY, creates bash tool instance on demand
- Add BASH_TOOL and SKILL_TOOL to specialToolNames set so they don't go
through the generic loadTools path (bash is created here, skill is
intercepted in handler before tool.invoke)
- Remove file name listing from skill content text — it's the skill
author's responsibility to disclose files in SKILL.md, not the framework
* feat: batch upload for skill files, replace sequential uploads
- Add batchUploadCodeEnvFiles() to crud.js: single POST to /upload/batch
with all files in one multipart request, returns shared session_id
- Rewrite primeSkillFiles to collect all streams (SKILL.md + bundled files)
then do one batch upload instead of N sequential uploads
- Replace uploadCodeEnvFile with batchUploadCodeEnvFiles across all callers
(handlers.ts, initialize.js, openai.js, responses.js)
* refactor: remove invokedSkillIds from conversation schema
Skills aren't re-loaded between runs, so conversation-level state for
invoked skills doesn't help. Skill state will live on messages instead
(like tool_search discoveredTools and summaries), enabling in-place
re-injection on follow-up runs.
Removes invokedSkillIds from: convo Mongoose schema, IConversation
interface, Zod schema, ToolExecuteOptions.updateConversation, and
all three caller wiring points.
* feat: smart skill file re-priming with session freshness checking
Schema:
- Add codeEnvIdentifier field to ISkillFile (type + Mongoose schema)
- Add updateSkillFileCodeEnvIds batch method (uses tenantSafeBulkWrite)
- Export checkIfActive from Code/process.js
Extraction:
- Add extractInvokedSkillsFromHistory() to run.ts — scans message
history for AIMessage tool_calls where name === 'skill', extracts
skillName args. Follows same pattern as extractDiscoveredToolsFromHistory.
Smart re-priming in primeSkillFiles:
- Before batch uploading, checks if existing codeEnvIdentifiers are
still active via getSessionInfo + checkIfActive (23h threshold)
- If session is still active, returns cached references (zero uploads)
- If stale or missing, batch-uploads everything and persists new
identifiers on SkillFile documents (fire-and-forget)
- Single session check covers all files (batch shares one session_id)
Wiring:
- Pass getSessionInfo, checkIfActive, updateSkillFileCodeEnvIds
through ToolExecuteOptions and all three controller entry points
* feat: wire skill file re-priming at run start via initialSessions
Flow:
1. initialize.js creates primeInvokedSkills callback with all deps
2. client.js calls it with message history before createRun
3. extractInvokedSkillsFromHistory scans for skill tool calls
4. For each invoked skill with files, primeSkillFiles uploads/checks
5. Returns initialSessions map passed to createRun
6. createRun passes initialSessions to Run.create (via RunConfig)
7. Run constructor seeds Graph.sessions, making skill files available
to subsequent bash/code tool calls via ToolNode session injection
Requires @librechat/agents with initialSessions on RunConfig (PR #94).
* refactor: use CODE_EXECUTION_TOOLS set for code tool checks
Import CODE_EXECUTION_TOOLS from @librechat/agents and replace inline
constant checks in handlers.ts and callbacks.js. Fixes missing bash
tool coverage in the session context injection (handlers.ts) and code
output processing (callbacks.js).
* refactor: move primeInvokedSkills to packages/api, add skill body re-injection
Moves primeInvokedSkills from an inline closure in initialize.js (with
dynamic requires) to a proper exported function in packages/api
skillFiles.ts with explicit typed dependencies.
Key changes:
- primeInvokedSkills now returns both initialSessions (for file priming)
AND injectedMessages (skill bodies for context continuity)
- createRun accepts invokedSkillMessages and appends skill bodies to
systemContent so the model retains skill instructions across runs
- initialize.js calls the packaged function with all deps passed explicitly
- client.js passes both initialSessions and injectedMessages to createRun
* fix: move dynamic requires to top-level module imports
Move primeInvokedSkills, getStrategyFunctions, batchUploadCodeEnvFiles,
getSessionInfo, and checkIfActive from inline requires to top-level
module requires where they belong.
* refactor: skill body reconstruction via formatAgentMessages, not systemContent
Replaces the lazy systemContent approach with proper message-level
reconstruction:
SDK (formatAgentMessages):
- New invokedSkillBodies param (Map<string, string>)
- Reconstructs HumanMessages after skill ToolMessages at the correct
position in the message sequence, matching where ToolNode originally
injected them
LibreChat:
- extractInvokedSkillsFromPayload replaces extractInvokedSkillsFromHistory
(works with raw TPayload before formatAgentMessages, not BaseMessage[])
- primeInvokedSkills now takes payload instead of messages, returns
skillBodies Map instead of injectedMessages
- client.js calls primeInvokedSkills BEFORE formatAgentMessages, passes
skillBodies through as the 4th param
- Removed invokedSkillMessages from createRun (no more systemContent hack)
- Single-pass: skill detection happens inside formatAgentMessages' existing
tool_call processing loop, zero extra message iterations
* refactor: rename skillBodies to skills for consistency with SDK param
* refactor: move auth loading into primeInvokedSkills, pass loadAuthValues as dep
The payload/accessibleSkillIds guard and CODE_API_KEY loading now live
inside primeInvokedSkills (packages/api) rather than in the CJS caller.
initialize.js passes loadAuthValues as a dependency and the callback
is only created when skillsCapabilityEnabled.
* feat: ReadFile tool + conditional bash registration + skill path namespacing
ReadFile tool (read_file):
- General-purpose file reader, event-driven (ON_TOOL_EXECUTE)
- Schema: { file_path: string } — "{skillName}/{path}" convention
- handleReadFileCall: resolves skill name from path, ACL check, reads
from DB cache or storage, binary detection, size limits (256KB),
lazy caching (512KB), line numbers in output
- SKILL.md special case: reads skill.body directly
- Dispatched alongside SKILL_TOOL in createToolExecuteHandler
- Added to specialToolNames in ToolService
Conditional tool registration:
- ReadFile + SkillTool: always registered when skills enabled
- BashTool: only registered when codeEnvAvailable === true
- codeEnvAvailable passed through InitializeAgentParams from caller
Skill file path namespacing:
- primeSkillFiles now uploads as "{skillName}/SKILL.md" and
"{skillName}/{relativePath}" instead of flat names
- Prevents file collisions when multiple skills are invoked
Wiring:
- getSkillFileByPath + updateSkillFileContent passed through
ToolExecuteOptions in all three callers
* feat: return images/PDFs as artifacts from read_file, tighten caching
Binary artifact support:
- Images (png, jpeg, gif, webp) returned as base64 in artifact.content
with type: 'image_url', processed by existing callback attachment flow
- PDFs returned as base64 artifact similarly
- Binary size limit: 10MB (MAX_BINARY_BYTES)
- Other binary files still return metadata + bash fallback
Caching:
- Text cached only on first read (file.content == null check)
- Binary flag cached only on first detection (file.isBinary == null)
- Skill files are immutable; no redundant cache writes
Registration:
- ReadFileToolDefinition now includes responseFormat: 'content_and_artifact'
* chore: update @librechat/agents to version 3.1.66-dev.0 and add peer dependencies in package-lock.json and package.json files
* fix: resolve review findings #1,#2,#4,#5,#6,#10,#13
Critical:
- #1: primeInvokedSkills now accumulates files across all skills into
one session entry instead of overwriting. Parallel processing via
Promise.allSettled.
- #2: codeEnvAvailable now computed and passed in openai.js and
responses.js (was missing, bash tool never registered in those flows)
Major:
- #4: relativePath in updateSkillFileCodeEnvIds now strips the
{skillName}/ prefix to match SkillFile documents. SKILL.md filter
uses endsWith instead of exact match.
- #5: File priming guarded on apiKey being non-empty (skip when not
configured instead of failing with auth error)
- #6: Skills processed in parallel via Promise.allSettled instead of
sequential for-of loop
Minor:
- #10: Use top-level imports in initialize.js instead of inline requires
- #13: Log warning when skill catalog reaches the 100-skill limit
* fix: resolve followup review findings N1,N2,N4
N1 (CRITICAL): Wire skill deps into responses.js non-streaming path.
Was completely missing getSkillByName, file strategy functions, etc.
N2 (MAJOR): Single batch upload for ALL skills' files. Resolves skills
in parallel (Phase 1), then collects all file streams across skills
and does ONE batchUploadCodeEnvFiles call (Phase 2). All files share
one session_id, eliminating cross-session isolation issues.
N4 (MINOR): Move inline require() to top-level in openai.js and
responses.js, consistent with initialize.js.
* fix: add mocks for new file strategy imports in controller tests
* fix: restore session freshness check, parallelize file lookups, add warnings
R1: Re-add session freshness check before batch upload. Checks any
existing codeEnvIdentifier via getSessionInfo + checkIfActive. If the
session is still active (23h window), returns cached file references
with zero re-uploads.
R2: listSkillFiles calls parallelized via Promise.all (were sequential
in the for-of loop).
R3: Log warning when skill record lookup fails during identifier
persistence (was a silent empty-string fallback).
* fix: guard freshness cache on single-session consistency
* fix: multi-session freshness check (code env handles mixed sessions natively)
The code execution environment fetches each file by its own
{session_id, fileId} pair independently — no single-session
requirement. Removed the sessionIds.size === 1 guard.
Now checks ALL distinct sessions for freshness. If every session
is still active (23h window), returns cached references with per-file
session_ids preserved. If any session expired, falls through to
re-upload everything in a single batch.
* perf: parallelize session freshness checks via Promise.all
* fix: add optional chaining for session info retrieval in primeInvokedSkills
Updated the primeInvokedSkills function to use optional chaining for getSessionInfo and checkIfActive methods, ensuring safer access and preventing potential runtime errors when these methods are undefined.
* fix: address review findings #1-#9 + Codex P1/P2 + session probe
Critical:
- #1/Codex P1: Add codeApiKey loading to openai.js and responses.js
loadTools configurable (was missing, file priming broken in 2/3 paths)
- Codex P1: Fix cached file name prefix in primeSkillFiles cache path
(was sf.relativePath, now ${skill.name}/${sf.relativePath})
Major:
- Codex P2: Honor ephemeral skills toggle in agents endpoint
(check ephemeralAgent?.skills !== false alongside admin capability)
- #4: Early size check using file.bytes from DB before streaming
(prevents full-file buffer for oversized files)
Minor:
- #5: Replace Record<string, any> with Record<string, boolean | string>
- #6: Localize Pin/Unpin aria-labels with com_ui_pin/com_ui_unpin
- #8: Parallelize stream acquisition in primeSkillFiles via
Promise.allSettled
- #9: Log warning for partial batch upload failures with filenames
Performance:
- Session probe optimization: getSessionInfo now hits per-object
endpoint (GET /sessions/{sid}/objects/{fid}) instead of listing
entire session (GET /files/{sid}?detail=summary). O(1) stat vs
O(N) list + linear scan.
* refactor: extract shared skill wiring helper + add unit tests
DRY (#3):
- New skillDeps.js exports getSkillToolDeps() with all 9 skill-related
deps (getSkillByName, listSkillFiles, getStrategyFunctions, etc.)
- Replaces 5 identical copy-paste blocks across initialize.js, openai.js,
responses.js (streaming + non-streaming paths)
- One place to maintain when skill deps change
Tests (#2):
- 8 unit tests for extractInvokedSkillsFromPayload covering:
string args, object args, missing skill tool_calls, non-assistant
messages, malformed JSON, empty skillName, empty payload, dedup
* fix: remove @jest/globals import, use global jest env
* fix: resolve round 2 review findings R2-1 through R2-7
R2-1 (toggle semantics): openai.js + responses.js now check admin
capability (AgentCapabilities.skills) alongside ephemeral toggle.
Aligns with initialize.js.
R2-2 (swallowed error): primeInvokedSkills now logs
updateSkillFileCodeEnvIds failures (was .catch(() => {}))
R2-4 (test cast): Record<string, string> → Record<string, unknown>
R2-5 (DRY regression): Extract enrichWithSkillConfigurable() into
skillDeps.js. Replaces 4 identical loadAuthValues blocks.
Each loadTools callback is now a one-liner. JSDoc added (R2-6).
R2-7 (sequential streams): primeInvokedSkills now uses
Promise.allSettled for parallel stream acquisition.
* fix: require explicit skills toggle + treat partial cache as miss
- initialize.js: change ephemeralSkillsToggle !== false to === true
(unset toggle no longer enables skills)
- primeSkillFiles cache: require ALL files to have codeEnvIdentifier
before using cache (partial persistence = cache miss = re-upload)
- primeInvokedSkills cache: same check (allFilesWithIds.length must
equal total file count)
* fix: pass entity_id=skillId on batch upload, eliminates per-user cache thrashing
primeSkillFiles now passes entity_id: skill._id.toString() to
batchUploadCodeEnvFiles. This scopes the code env session to the
skill, not the user. All users sharing a skill share the same
uploaded files — no more cache thrashing from overwriting each
other's codeEnvIdentifier.
The stored codeEnvIdentifier now includes ?entity_id= suffix so
freshness checks pass the entity_id through to the per-object
stat endpoint. Both primeSkillFiles and primeInvokedSkills
store consistent identifier formats.
* fix: pass entity_id on multi-skill batch upload, consistent identifier format
* Revert "fix: pass entity_id on multi-skill batch upload, consistent identifier format"
This reverts commit c85ce2161e6f608bbebc432e72f2241d0f572517.
* refactor: per-skill upload in primeInvokedSkills, eliminate multi-skill batch
Replace the monolithic multi-skill batch upload with per-skill
primeSkillFiles calls. Each skill gets its own session with
entity_id=skillId, ensuring:
- Correct session auth (entity_id matches on freshness checks)
- Per-skill freshness caching (only expired skills re-upload)
- Shared skill sessions work across users (same entity_id=skillId)
- Code env handles mixed session_ids natively
The big batch block (stream collection, single upload, identifier
mapping) is replaced by a simple loop over primeSkillFiles, which
already handles freshness caching, batch upload, and identifier
persistence per-skill.
* fix: resolve review findings #1,#3-5,#7,#9-11
Critical:
- #1: Strip ?entity_id= query string before splitting codeEnvIdentifier
into session_id/fileId (was corrupting cached file IDs in 4 locations)
Major:
- #4: Parallelize per-skill primeSkillFiles via Promise.allSettled
- #5: Add logger.warn to all empty .catch(() => {}) on cache writes
Minor:
- #7: Add logger.debug to enrichWithSkillConfigurable catch block
- #9: Use error instanceof Error guard in batchUploadCodeEnvFiles
- #10: Move enrichWithSkillConfigurable to TypeScript in packages/api
(skillConfigurable.ts), skillDeps.js wraps with loadAuthValues dep
- #11: Reduce MAX_BINARY_BYTES from 10MB to 5MB (~11.5MB peak with b64)
* fix: forward entity_id in session probe + always register bash tool
Codex P2 (entity_id in probe): getSessionInfo now preserves and
forwards query params (including entity_id) to the per-object stat
endpoint. Without this, identifiers stored as ...?entity_id=... would
fail auth checks because the entity_id scope was dropped.
Codex P2 (bash tool availability): Remove codeEnvAvailable gate from
injectSkillCatalog. Bash tool definition is now always registered when
skills are enabled. Actual tool instance creation still happens at
execution time in loadToolsForExecution (which loads per-user
credentials). This ensures users with per-user CODE_API_KEY get
bash without requiring a global env var at init time.
Removes codeEnvAvailable from InjectSkillCatalogParams,
InitializeAgentParams, and all three controller entry points.
* fix: add debug logging to primeInvokedSkills catch, rename export alias
* fix: stub bash tool when no key + remove PDF artifact path
Codex P1 (bash tool): When CODE_API_KEY is unavailable, create a stub
tool that returns "Code execution is not available. Use read_file
instead." This prevents "tool not found" errors from the model
repeatedly calling bash_tool in no-code-env deployments while still
registering the definition for per-user credential users.
Codex P2 (PDF artifacts): Remove PDF image_url artifact path. The
host artifact pipeline processes image_url via saveBase64Image which
fails for PDFs. PDFs now fall through to the generic binary handler
("Use bash to process"). TODO comment for future document artifact
support.
Also: isImageOrPdf → isImage in early size checks (PDFs are no
longer treated as artifact candidates).
* fix: remove dead PDF_MIME constant, hoist skillToolDeps, document session_id
- #7: Remove unused PDF_MIME constant (dead code after PDF artifact removal)
- #11: Hoist skillToolDeps to module-level constant (avoid per-call allocation)
- #6: Document that CodeSessionContext.session_id is a representative value;
ToolNode uses per-file session_id from the files array
* fix: call toolEndCallback for skill/read_file artifacts + clear codeEnvIdentifier on re-upload
Codex P1 (toolEndCallback bypass): skill and read_file handler branches
returned early, bypassing the toolEndCallback that processes artifacts
(image attachments). Now calls toolEndCallback when the result has an
artifact, using the same metadata pattern as the normal tool.invoke path.
Codex P1 (stale identifiers): upsertSkillFile now $unset's
codeEnvIdentifier alongside content and isBinary when a file is
re-uploaded. Prevents the freshness cache from returning references
to old file content after a skill file is replaced.
* fix: add session_id comment at cached path, rename skillResult to handlerResult
* fix: return content_and_artifact from bash stub so result.content is populated
* fix: deterministic skill lookup, dedup warning, and multi-session freshness check
- getSkillByName: add sort({updatedAt:-1}) so name collisions resolve
deterministically to the most recently updated skill
- injectSkillCatalog: warn when multiple accessible skills share a name
- primeSkillFiles: check ALL distinct sessions for freshness, not just
the first file's session, preventing stale refs after partial bulkWrite
* refactor: update icon import in Skills component
- Replaced the Scroll icon with ScrollText in the Skills component for improved clarity and consistency in the UI.
* fix: SKILL.md cache parity, gate bash_tool on code env, fix read_file too-large message
- primeSkillFiles: filter SKILL.md from returned files array on fresh
upload so cached and non-cached paths return identical file sets
(SKILL.md is still on disk in the session for bash access)
- injectSkillCatalog: only register bash_tool when codeEnvAvailable is
true; thread the flag from all three CJS callers via execute_code
capability check
- handleReadFileCall: tell the model to invoke the skill first before
suggesting /mnt/data paths for oversized files
* fix: use EnvVar constant, deduplicate auth lookup, validate batch upload, stream byte limit
- Replace hardcoded 'LIBRECHAT_CODE_API_KEY' with EnvVar.CODE_API_KEY
in skillConfigurable.ts and skillFiles.ts
- Resolve code API key once at run start in initialize.js and pass to
both primeInvokedSkills and enrichWithSkillConfigurable via optional
preResolvedCodeApiKey param, eliminating redundant loadAuthValues calls
- Add response structure validation in batchUploadCodeEnvFiles before
accessing session_id/files to surface unexpected responses early
- Add streaming byte counter in handleReadFileCall that aborts and
destroys the stream when accumulated bytes exceed MAX_BINARY_BYTES,
preventing full file buffering when DB metadata is inaccurate
* refactor: update icon import in ToolsDropdown component
- Replaced the Scroll icon with ScrollText in the ToolsDropdown component for improved clarity and consistency in the UI.
* fix: partial upload failure detection, EnvVar in initialize.js, declaration ordering
- primeSkillFiles: return null (failure) when batch upload partially
succeeds — missing bundled files would cause runtime bash/read
failures with missing paths in code env
- initialize.js: replace hardcoded 'LIBRECHAT_CODE_API_KEY' with
EnvVar.CODE_API_KEY imported from @librechat/agents
- initialize.js: move enabledCapabilities, accessibleSkillIds, and
codeApiKey declarations before the toolExecuteOptions closure that
references them (eliminates reliance on temporal dead zone hoisting)
* 🎨 feat: Skills UI — Create/Edit/Share/List with Conditional File Tree
First-pass UI on top of the CRUD API scaffolding (#12613). Ships the full
user-facing flow for inline, single-SKILL.md skills and leaves a clean
drop-in for phase-2 multi-file support.
- Create a skill from /skills/new with name (kebab-case, validated),
description, and SKILL.md body — wired to the real `useCreateSkillMutation`
and `TCreateSkill` payload.
- List skills in a sidebar (SkillsSidePanel) via `useListSkillsQuery` with
live search filtering.
- Edit any skill the caller has EDIT permission on — `useUpdateSkillMutation`
passes `expectedVersion` for optimistic concurrency and surfaces 409
conflicts as a warning toast + cache refetch.
- Non-blocking `TSkillWarning[]` (e.g. "description too short") are shown
inline above the form after a successful create/patch.
- Read-only mode when the current user lacks EDIT — the form still renders
but inputs are marked `readOnly` and the save/reset buttons are hidden.
- Share via ACL using the existing `GenericGrantAccessDialog` — the
`ShareSkill` button is gated on the SHARE permission.
- Delete with confirmation, driven by `useDeleteSkillMutation({ id })`.
- Conditional file tree: only rendered when `useListSkillFilesQuery`
returns > 0 files. The tree groups flat `relativePath` strings into a
nested view (no `react-arborist` dependency) and supports per-file
deletion via `useDeleteSkillFileMutation`. Upload is intentionally
deferred — the backend stubs it at 501 in phase 1.
- New routes: `/skills`, `/skills/new`, `/skills/:skillId`.
- Sidebar accordion (`SkillsAccordion` wrapping `SkillsSidePanel`) added
to `useSideNavLinks` gated on `PermissionTypes.SKILLS` USE.
The initial UI branch (#12580) shipped a lot of exploration code on top of
a now-superseded placeholder backend. Kept as complementary: the `Skills/`
component tree, translation keys, role descriptions, `PublicSharingToggle`
SKILL mapping, `resources.ts` SKILL config, `useCanSharePublic` SKILL
mapping, and `data-provider/roles.ts` `useUpdateSkillPermissionsMutation`.
Deferred out of this first pass:
- Skill favorites (`useSkillFavorites`, `getSkillFavorites` endpoint) —
the backend route doesn't exist yet; saving for a follow-up.
- AgentConfig `SkillSelectDialog` integration — the UI branch had this
gated behind `false &&`; rolled back with the config.
- `InvocationMode` / `CategorySelector` / `parseSkillMd` / tree-node
mutations — not in the Anthropic skill spec and not in the CRUD API.
- `react-arborist` dependency — replaced with a hand-rolled recursive
tree built from flat `TSkillFile[]`.
- 38 data-schemas skill model tests: pass
- 25 api skill route tests: pass
- 16 user-controller cleanup tests: pass
* 🔐 feat: Default-On Skills in Interface Config and Role Seeder
The skills accordion was registered in the side nav gated on
`PermissionTypes.SKILLS` USE, but no one was actually seeding that
permission on startup, so a fresh install had the USER role with
zero skill permissions and the accordion never rendered.
Fixes three gaps:
1. `interfaceSchema` in data-provider's `config.ts` had no `skills`
field at all. Added it alongside the existing agents/prompts shape
(boolean | { use, create, share, public }) and a default of
`{ use: true, create: true, share: false, public: false }`.
2. `loadDefaultInterface` in data-schemas passed every interface key
through to the loaded config EXCEPT `skills`. Added the one-line
passthrough so `appConfig.interfaceConfig.skills` is actually
populated on boot.
3. `updateInterfacePermissions` in packages/api/src/app/permissions.ts
seeds role permissions from the interface config on every restart.
Added:
- `SKILLS` case to `hasExplicitConfig`
- `skillsDefaultUse/Create/Share/Public` extraction (mirrors
prompts/agents)
- `PermissionTypes.SKILLS` block in `allPermissions` that falls
through config → roleDefaults → schema default, same pattern as
AGENTS and PROMPTS
- `SKILLS` entry in the share-backfill array so that pre-existing
SKILL role docs missing SHARE/SHARE_PUBLIC get them filled on the
next restart
Test expectations updated: seven `expectedPermissionsFor(User|Admin)`
blocks in `permissions.spec.ts` now include SKILLS, matching the
role-default values (USER: use+create true, share/public false;
ADMIN: all true).
Result: on a fresh install, a regular USER gets skill USE/CREATE
and the "Skills" accordion shows up in the chat side panel without
any yaml config. Admins can lock it down per role or per tenant via
`interface.skills` in librechat.yaml.
Tests:
- 34 packages/api permissions.spec.ts: pass
- 151 packages/api app tests: pass
- 38 data-schemas skill.spec.ts: pass
- 928 data-provider tests: pass
- 25 api skills.test.js: pass
* ♻️ fix: Resolve Skills UI Review Findings
Addresses the 13 findings from the PR review against the prior commit.
1. **canEdit consistency** — extracted `useSkillPermissions(skill)` as the
single source of truth for owner/admin/ACL gating. `SkillsView`,
`SkillForm`, `ShareSkill` all consume it; `SkillFileTree`'s per-file
delete button now honors admin + EDIT-bit permissions instead of just
ownership. Unit tests cover owner, admin, editor-ACL, viewer-ACL,
owner-ACL, loading, and undefined-skill cases.
2. **Disabled submit buttons** — create/edit form submit buttons now set
native `disabled` (not just `aria-disabled`) during `isLoading`.
`onSubmit` also guards with an early return when the mutation is still
in-flight so a duplicate enter-key submit can't create two skills.
3. **Wrong maxLength error message** — description/name `maxLength` rules
no longer re-use `com_ui_skill_*_required`. Added dedicated
`com_ui_skill_name_too_long` and `com_ui_skill_description_too_long`
keys with the literal limit interpolated (`{{0}}`).
4. **Search debouncing** — `SkillsSidePanel` now threads the filter input
through the existing `useDebounce` hook (250ms) so typing "skills" no
longer fires six separate list queries.
5. **Frontend test coverage** — added:
- `tree.test.ts` (9 tests) covering `buildTree` / `nodeKey` edge cases:
empty input, single root file, multiple roots, nested folders,
deeply-nested trees, lexicographic sort, empty paths, stable keys
- `useSkillPermissions.test.ts` (7 tests) covering every precedence
branch (owner / admin / EDIT / VIEW / owner-ACL / loading / undef)
Form integration tests proved flaky against react-hook-form's async
`isValid` with our jest-dom mock setup; deferred to a follow-up PR
with a proper `@librechat/client` test harness.
6. **Shared `SKILL_NAME_PATTERN`** — promoted the regex plus the four
length constants (`SKILL_NAME_MAX_LENGTH`,
`SKILL_DESCRIPTION_MAX_LENGTH`, `SKILL_DESCRIPTION_SHORT_THRESHOLD`,
`SKILL_DISPLAY_TITLE_MAX_LENGTH`, `SKILL_BODY_MAX_LENGTH`) out of
`packages/data-schemas/src/methods/skill.ts` and into
`packages/data-provider/src/types/skills.ts`. The data-schemas
module now aliases the shared exports so the backend validator and
the frontend form share one source of truth. Also fixed a latent bug:
the client regex was stricter than the backend
(`^[a-z0-9]+(?:-[a-z0-9]+)*$` vs. the real `^[a-z0-9][a-z0-9-]*$`),
which would have rejected valid names like `foo--bar` client-side.
7. **Removed hardcoded "Claude"** — replaced `com_ui_skill_description_help`
("Claude uses this to...") with a new `com_ui_skill_create_subtitle`
for the form header and `com_ui_skill_description_field_hint`
("This is what the model reads to decide...") for the inline hint.
LibreChat is LLM-agnostic; the old copy misled GPT/Gemini users.
8. **Lifted tree mutation hook** — `useDeleteSkillFileMutation` is now
instantiated once in `SkillFileTree` (not per `TreeRow`). A
`TreeContext` provides `onDeleteFile` + `isDeleting` + `canEdit` to
rows. A 60-node tree used to instantiate 60 mutation hooks; it now
instantiates one.
9. **List O(n) re-render** — `SkillListItem` no longer reads
`useParams()` directly. `SkillList` reads the active id once and
passes `isActive` as a prop, so navigation only re-renders the two
items whose `isActive` flipped (memo'd), not all N items.
10. **Deduped help text** — the field-level hint and form-level subtitle
now use different translation keys with distinct copy instead of
showing the same sentence twice on the same page.
11. **Removed ineffective `useCallback`** — `DeleteSkill.handleDelete`,
`CreateSkillForm.onSubmit` / `.handleCancel`, `SkillForm.onSubmit`,
and `SkillFileTree.handleDeleteFile` all wrapped closures around
React Query `mutation` refs, whose identities change every render.
Their dep arrays invalidated every render, making the memo a no-op
with extra overhead. `SkillFileTree` now destructures the stable
`mutate` function and inlines the arrow inside the memoized
`contextValue` — one stable reference per deps change.
12. **Import order** — fixed shortest→longest package ordering and
longest→shortest local ordering across all touched skill files per
AGENTS.md. `react` always first where imported.
13. **Memoization principle** — documented the rule with inline comments:
`memo` on components that appear in repeated contexts (`TreeRow`,
`SkillListItem`) or as children of frequently-re-rendering parents
(`ShareSkill` / `DeleteSkill` under `SkillForm`'s per-keystroke
form-state updates). Removed `memo` from `SkillFileTree` since its
parent `SkillDetailPanel` only re-renders on query-data changes.
- 38 data-schemas skill.spec.ts
- 34 packages/api permissions.spec.ts
- 25 api skills.test.js
- 16 client unit tests (9 buildTree + 7 useSkillPermissions)
- All type-checks + eslint clean on touched files
* 🧹 fix: Skills Duplication, Input Styling, Remove LLM-specific Copy
Three UI fixes from an in-chat review pass:
1. **Sidebar duplication** — `SkillsView` was rendering its own
`SkillsSidePanel` aside alongside the chat side panel's
`SkillsAccordion`, so on `/skills` the user saw the skill list
twice. Fixed by mirroring the `InlinePromptsView` pattern: the
route content is now just the detail / create panel and the
chat side panel is the sole list. Added `/skills → /skills/new`
redirect and a `/skills/new` literal route so `useParams().skillId`
is `undefined` for "new" (matches prompts).
2. **Name Input styling** — the big floating-label pattern used by
prompts/agents for the primary name field was replaced with a
conventional `<Label>` + `<Input>` above it, diverging from the
rest of the app. Restored the prompts-style `text-2xl` input with
the peer-focus animated label on both `CreateSkillForm` and
`SkillForm`. Kept the conventional pattern for description and
body since they're textareas.
3. **Remove LLM-specific copy from skill translations** — dropped
`com_ui_skill_description_help` ("Claude uses this to...") and
the transitional "This is what the model reads..." phrasing.
Field hint is now a neutral "Be specific about when this skill
should apply." and the create-page subtitle is a neutral "Author
a new skill your agents can invoke." LibreChat is LLM-agnostic;
baking product names into user-facing copy is wrong outside the
`com_endpoint_anthropic_*` keys where the setting actually only
applies to Claude models.
Side-effect: the `SkillDetailView` wrapper in `SkillsView` now only
renders the file-tree aside when the skill has > 0 files — same
conditional-tree behavior as before, just scoped to this route
instead of also trying to also render a list sidebar.
- 16 client skill tests still pass
- Type-check + eslint clean on touched files
* 🎁 feat: Restore Skills UI from PR #12580
Brings back everything the original UI PR (#12580, commit da039917c)
shipped that my earlier rebase dropped. Verbatim restores where possible;
adapts the new hooks/types where the backend contract has shifted.
**Scoped-out / gated-off (now restored as inert UI scaffolding):**
- `hooks/useSkillFavorites.ts` + `utils/favoritesError.ts` + the
`useGetSkillFavoritesQuery` / `useUpdateSkillFavoritesMutation` additions
in `data-provider/Favorites.ts`. The backend route doesn't exist yet —
the data-service functions resolve with empty arrays so the Star UI is a
visual-only no-op until phase 2.
- `dialogs/SkillSelectDialog.tsx` + the "Add Skills" section in
`SidePanel/Agents/AgentConfig.tsx` (still gated behind the original
`false &&`) + `skills?: string[]` on `AgentForm` / `Agent` /
`AgentCreateParams` / `AgentUpdateParams` + the `skills: []` entry in
`defaultAgentFormValues`.
- `TUserFavorite.skillId` reserved on the shared favorites type.
**Concept-is-gone / deleted-types (restored as UI-only types + stubs):**
- `InvocationMode` enum and `TSkillNode`, `TSkillTreeResponse`,
`TCreateSkillNodeRequest`, `TUpdateSkillNodeRequest` types in
`packages/data-provider/src/types.ts`. UI-facing only; the backend flat
`TSkillFile[]` contract is unchanged.
- `TSkill.invocationMode?: InvocationMode` as an optional field. Forms
read/write it in local state and deliberately drop it from the PATCH
payload until the backend column lands.
- `tree/SkillFileTree.tsx` (`react-arborist`-based), `SkillTreeNode.tsx`,
`TreeToolbar.tsx`, `SkillFileEditor.tsx`, `SkillFilePreview.tsx` — full
filesystem-style browser UI restored verbatim.
- `data-provider/Skills/tree-queries.ts` + `tree-mutations.ts` hooks
(`useGetSkillTreeQuery`, `useCreateSkillNodeMutation`, etc.). The
`data-service` stubs them: `getSkillTree` returns `{ nodes: [] }`,
`createSkillNode` / `updateSkillNode` / `updateSkillNodeContent` return
synthetic node shapes, `deleteSkillNode` resolves void. Hooks compile
and run; tree is empty until phase 2 wires a real backend.
- `MutationKeys.createSkillNode` / `updateSkillNode` / `deleteSkillNode` /
`updateSkillNodeContent` + `CreateSkillNodeBody` /
`UpdateSkillNodeVariables` / `DeleteSkillNodeBody` /
`UpdateSkillNodeContentVariables` types.
- `QueryKeys.skillTree` / `skillNodeContent` / `skillFavorites` /
`favorites` and the `skillTree()` endpoint helper.
**Scope-simplified (restored with minimal adaptation):**
- `display/SkillDetailHeader.tsx` + `display/SkillDetail.tsx`. Header now
falls back to `InvocationMode.auto` when `skill.invocationMode` is
undefined.
- `forms/SkillContentEditor.tsx` — click-to-edit markdown preview toggle
for the SKILL.md body field. Wired into both `CreateSkillForm` and
`SkillForm` replacing the plain `<TextareaAutosize>`.
(Needed `@ts-ignore` on `remarkPlugins` / `rehypePlugins` for the same
`PluggableList` vs `Pluggable[]` shape drift `MarkdownLite.tsx` already
works around.)
- `forms/InvocationModePicker.tsx` + `forms/CategorySelector.tsx` — the
auto/manual/both dropdown and the skill category selector. Wired into
both forms inside a `FormProvider` so the Controller-based widgets can
read `useFormContext`. `category` flows to the PATCH / POST payload as
before; `invocationMode` is UI-only per the type note above.
- `buttons/CreateSkillMenu.tsx` + `utils/parseSkillMd.ts` — dropdown with
AI / Manual / Upload SKILL.md entries + the YAML frontmatter parser for
the upload path. `CreateSkillForm.defaultValues` now accepts the parsed
shape, so the upload → redirect → pre-populated form flow works again.
- `buttons/AdminSettings.tsx` — admin permissions dialog. Uses the
existing `useUpdateSkillPermissionsMutation` which was already wired.
- `sidebar/FilterSkills.tsx` — restored filter + AdminSettings +
CreateSkillMenu wrapper. `SkillsSidePanel.tsx` is back to the original
`FilterSkills`-based layout.
- `lists/SkillList.tsx` + `lists/SkillListItem.tsx` — restored verbatim.
- `layouts/SkillsView.tsx` — restored the full tree + file editor + file
preview layout. The chat side panel keeps its own accordion list; this
view is the inline detail experience.
- `hooks/Generic/useUnsavedChangesPrompt.ts` — route-leave guard hook.
- `useGetSkillByIdQuery` is aliased to `useGetSkillQuery` so restored
components (`SkillsView`, `SkillForm`) that import the old name resolve
to the new hook.
- `SkillSelectDialog` + `AgentConfig` coerce `skillsData?.skills` instead
of `.data` (list response shape drift from the CRUD PR).
- `CreateSkillForm` / `SkillForm` wrap their JSX in `FormProvider` so the
restored `CategorySelector` and `SkillContentEditor` components —
which read `useFormContext` — work inside the existing forms without
another refactor.
- `CreateSkillForm.defaultValues` prop accepts `Partial<Values> &
{ invocationMode?: unknown }` so the upload flow's
`{ name, description, invocationMode }` shape passes through cleanly.
- `SkillsView` route map gains `/skills/:skillId/edit` and
`/skills/:skillId/file/:nodeId` so the tree-navigation URLs the original
view produces actually resolve.
- `client/package.json` gains `react-arborist@^3.4.3`.
- ~60 translation keys the restored files reference — invocation labels,
edit/create page titles, file editor chrome, tree toolbar tooltips,
favorites, admin allow-settings, unknown-file-type, sr_public_skill,
delete/rename _var variants — all added to `en/translation.json`.
- Prompts-style floating-label name input — kept from my earlier commit
so it matches the rest of the app (user reviewed and approved that
styling). Hidden skill-body textarea is replaced by `SkillContentEditor`
in both forms.
- 38 data-schemas skill.spec.ts
- 34 packages/api permissions.spec.ts
- 25 api skills.test.js
- 7 client useSkillPermissions.test.ts
- Type-check: pre-existing error count (188) dropped to 120 because my
restorations fixed some previously-broken field types.
* chore: Update package-lock.json to include react-arborist and memoize-one
* feat: Add support for react-arborist in Vite configuration
This update introduces a new condition in the Vite configuration to handle the 'react-arborist' package, ensuring it is properly recognized during the build process. This change enhances compatibility with the recently added 'react-arborist' dependency in the project.
* 🩹 fix: Hide InvocationMode, Fix SkillContentEditor Click-to-Edit
1. Hide InvocationModePicker from both CreateSkillForm and SkillForm.
Component stays on disk for when the backend lands the column.
2. Fix "Click to edit" doing nothing on SkillContentEditor. The
`onBlur={() => setIsEditing(false)}` on the TextareaAutosize was
racing with `autoFocus` — React renders the textarea, autoFocus
fires, then a layout/reconciliation blur fires immediately,
bouncing back to preview mode before the user can interact.
Removed onBlur; users toggle via the header button or Escape key.
* 🎨 feat: Reader-First Skills UI — Match Claude.ai Layout
Reworks the Skills UI from form-first to reader-first, matching
Claude.ai's skill detail pattern.
**Default view is now read-only.** Clicking a skill in the sidebar
navigates to `/skills/:id` which renders `SkillDetail` — a clean
content view with:
- Skill name as the primary heading
- Metadata row: "Added by" + "Last updated" (formatted date)
- Description block
- Rendered SKILL.md body in a bordered card with a source/rendered
toggle (eye + code icons, matching Claude.ai's segmented control)
No form fields, no save/cancel buttons. The user reads the skill
first and takes action deliberately.
**Create is now a dialog.** The `/skills/new` route is gone.
`CreateSkillMenu` (the + dropdown in the sidebar) now opens
`CreateSkillDialog` — a minimal modal with name, description, and
instructions fields. Upload-from-file still works: parse → populate
dialog → create. Matches Claude.ai's "Write skill instructions"
modal.
**Edit is behind an action.** The detail view shows an "Edit" button
(permission-gated) that navigates to `/skills/:id/edit`, rendering
the existing `SkillForm`. The edit route is preserved for direct
linking.
**Navigation goes to detail, not edit.** `SkillListItem` now
navigates to `/skills/:id` (detail) instead of `/skills/:id/edit`.
- `display/SkillMarkdownRenderer.tsx` — shared ReactMarkdown
component extracted from `SkillContentEditor`. Same remark/rehype
plugins, no form dependency.
- `display/SkillDetail.tsx` — the reader-first view (replaces the
old thin wrapper).
- `dialogs/CreateSkillDialog.tsx` — OGDialog modal for skill
creation.
- `layouts/SkillsView.tsx` — gutted and rebuilt. Three states:
no-skill (empty state), skillId (SkillDetail), skillId+edit
(SkillForm). Removed full-page CreateSkillForm, removed TreeView.
- `buttons/CreateSkillMenu.tsx` — opens dialog instead of navigating
to `/skills/new`. Upload flow: parse → set dialog defaults → open.
- `lists/SkillListItem.tsx` — navigate to detail, not edit.
- `routes/index.tsx` — removed `/skills/new` and file/nodeId routes;
`/skills` renders SkillsView directly (empty state).
- `display/index.ts`, `dialogs/index.ts` — added new exports.
- `locales/en/translation.json` — added ~10 new keys for metadata,
toggle labels, dialog title, empty state.
* 🩹 fix: SkillContentEditor click-to-edit z-index — button was z-0 behind rendered content
* 🩹 fix: Align Edit button size with Share/Delete (size-9)
* 🎨 feat: Claude.ai-Style Skill List Panel
Rewrites the skills sidebar to match Claude.ai's panel layout:
- Header: "Skills" title + search icon (toggles input) + add icon
(opens CreateSkillDialog directly, no dropdown menu)
- Collapsible "Skills" section with chevron toggle
- Skill items: 24px icon badge (rounded square with ScrollText icon)
+ name only. No description text in the list — that lives in the
detail view. Active item gets highlighted bg + bold font.
- Removed AdminSettings button from sidebar header — admin config
is accessible via the admin dashboard, not cluttering every user's
skill list.
- Removed FilterSkills wrapper (was Filter + AdminSettings +
CreateSkillMenu). The search + create are now inline in the panel
header.
Files changed:
- sidebar/SkillsSidePanel.tsx — full rewrite
- sidebar/SkillsAccordion.tsx — simplified wrapper
- lists/SkillList.tsx — collapsible section, no description
- lists/SkillListItem.tsx — icon badge + name, memo'd
* 🎨 fix: Align Skills UI Styling with Prompts Patterns
Style alignment pass based on direct comparison with claude.ai and
the existing prompts preview dialog.
SkillsSidePanel search now replaces the title in the header row when
toggled (search icon + input + X close), matching Claude.ai's pattern.
Previously it pushed a separate input below the header, wasting
vertical space. Close button clears the search term.
Replaced `text-text-tertiary` with `text-text-secondary` across
SkillDetail, SkillList, SkillForm, CreateSkillForm, CreateSkillDialog,
SkillContentEditor. Tertiary was too dark / low contrast.
SkillList section chevron label now reads "Personal skills" (matching
Claude.ai) via the existing `com_ui_my_skills` key, instead of the
generic "Skills" which duplicated the header.
Aligned with `PromptDetailHeader` styling:
- 48px round icon (ScrollText in bg-surface-secondary circle)
- Name + public badge in the icon row
- Metadata below the icon: User icon + author, Calendar icon + date
(text-xs text-text-secondary with gap-3, matching prompts exactly)
- Description uses the same label-above-text pattern as prompts
- Content card uses `bg-transparent` border (not bg-surface-primary-alt)
- Toggle buttons use size-5 icons and text-text-secondary for inactive
Changed from `max-w-lg p-0` to `max-w-5xl` with the same max-height
and padding pattern as the prompts PreviewPrompt dialog:
`max-h-[80vh] p-1 sm:p-2 gap-3 sm:gap-4`. Close button now renders
via default OGDialogContent behavior (removed showCloseButton=false).
* 🩹 fix: SkillDetail fills parent height, tighter spacing (px-6 pb-6 gap-2)
* 🩹 fix: Align Skills panel header padding (px-4) with list content below
* 🩹 fix: Reduce Skills header top padding (pt-2) to align with sidebar icon strip
* 🩹 fix: Tighten Skills header (py-2) and detail top (py-2) to align with sidebar icons and match edit view
* 🩹 fix: Offset SidePanel Nav pt-2 with -mt-2 on SkillsAccordion so Skills header aligns with icon strip
* 🛠️ fix: Increase Node memory limit for production build in package.json
* 🩹 fix: Remove top padding from SkillDetail header row (py-2 → pb-2)
* 🏗️ refactor: Move pt-2 from SidePanel/Nav wrapper to each panel
Removed the global `pt-2` from `SidePanel/Nav.tsx` and pushed it
into each panel's own top-level wrapper. This lets each panel own
its vertical alignment independently — Skills can sit flush at the
top to align with the sidebar icon strip, while other panels keep
their original spacing.
Panels updated with `pt-2`:
- PromptsAccordion (via className on PromptSidePanel)
- BookmarkPanel
- FilesPanel
- MemoryPanel
- MCPBuilderPanel
- AgentPanel (form wrapper)
- AssistantPanel (form wrapper)
- ParametersPanel (already had pt-2)
SkillsAccordion: removed the -mt-2 hack, now naturally flush.
* 🧹 fix: Align CreateSkillDialog field styling + remove 19 unused i18n keys
Dialog fields: all three inputs now use consistent `rounded-xl
border-border-medium px-3 py-2 text-sm` styling. Replaced the
`<Input>` component with a plain `<input>` to avoid the component's
built-in `rounded-lg border-border-light` overriding the dialog's
border style. Labels use `font-medium` for consistency.
Removed 19 unused translation keys from translation.json:
com_ui_skill_body, com_ui_skill_body_placeholder,
com_ui_skill_create_subtitle, com_ui_skill_file_delete_confirm,
com_ui_skill_file_delete_error, com_ui_skill_file_deleted,
com_ui_skill_files_empty, com_ui_skill_files_multi_hint,
com_ui_skill_list, com_ui_skill_load_error,
com_ui_skill_resize_file_tree, com_ui_skill_select_file,
com_ui_skill_select_file_desc, com_ui_skills_load_error,
com_ui_add_first_skill, com_ui_create_skill_page,
com_ui_edit_skill_page, com_ui_save_skill, com_ui_no_skills_title
* 🎁 feat: Upload Skill Dialog + Simplified Create Menu
New `UploadSkillDialog` matching Claude.ai's upload modal:
- Dashed drop zone with drag-and-drop support
- Accepts .md, .zip, .skill files
- Phase 1: processes .md files (parses YAML frontmatter → creates
skill with body as the full file content)
- Shows file requirements below the drop zone
- On success: navigates to the new skill's detail view
`CreateSkillMenu` now has two flat options (no sub-menu):
- "Write skill instructions" → opens `CreateSkillDialog`
- "Upload a skill" → opens `UploadSkillDialog`
Removed the disabled "Create with AI" option and the old file input
hidden-element approach. The sidebar `+` button now renders
`CreateSkillMenu` directly instead of a standalone create dialog.
- Removed 5 unused i18n keys (com_ui_skill_added_by,
com_ui_skill_last_updated, com_ui_skills_add_first,
com_ui_skills_filter_placeholder, com_ui_skills_new)
- Tightened metadata gap in SkillDetail (mt-1 → mt-0.5)
- Added 7 new upload-related i18n keys
* 🔒 feat: Zip/Skill File Upload Support with Safety Limits
Rewrites UploadSkillDialog to properly handle all three accepted
file types:
- `.md` — reads as text, parses YAML frontmatter, creates skill
- `.zip` / `.skill` — reads as ArrayBuffer, extracts with JSZip,
finds SKILL.md (at root or one level deep), parses its content,
creates skill. Shows spinner during processing.
Security guards against zip bombs:
- MAX_ZIP_SIZE: 50MB compressed file limit
- MAX_ENTRIES: 500 file limit inside the archive
- Path traversal rejection: skips entries with `..` or leading `/`
- SKILL.md search limited to depth ≤ 2 segments
Added `jszip@^3.10.1` to client dependencies (already in the
monorepo's node_modules from backend usage).
The name is inferred from the zip filename if SKILL.md frontmatter
doesn't have one (e.g. `skills-autofix.zip` → `skills-autofix`).
* 🚀 feat: Backend Skill Import + Live File Upload Endpoints
New endpoint that accepts a single multipart file (.md, .zip, .skill)
and creates a skill with all its files in one request:
- **.md**: parse YAML frontmatter → create skill with body
- **.zip / .skill**: extract with JSZip, find SKILL.md (root or one
level deep), create skill from its content, then persist every
additional file via `upsertSkillFile` + local file storage strategy.
Returns the created skill + an `_importSummary` with per-file
results.
Security:
- 50MB compressed file size limit (multer)
- 500 max entries in archive
- 10MB per individual file
- Path traversal rejection (no `..`, no absolute, validated charset)
- File type filter: only .md/.zip/.skill accepted
- Rate limited via existing `fileUploadIpLimiter` + `fileUploadUserLimiter`
Handler lives in `packages/api/src/skills/import.ts` with injectable
deps (`createSkill`, `upsertSkillFile`, `saveBuffer`) for testability.
Replaced the 501 stub with a real handler:
- Accepts multipart FormData with `file` + `relativePath`
- Saves file via local storage strategy
- Calls `upsertSkillFile` to persist the SkillFile record
- Returns the upserted document
- Rate limited, ACL-gated (EDIT permission required)
- 10MB per file limit
`UploadSkillDialog` now sends the file to `/api/skills/import` via
`dataService.importSkill(formData)` — no more client-side JSZip.
Removed `jszip` from client dependencies (only backend needs it).
Added `importSkill()` in data-service + `importSkill()` endpoint
builder in api-endpoints.
Updated the file upload test from expecting 501 stub to expecting
400 "no file provided" (live validation). All 25 skill route tests
pass.
* 🔒 fix: Complete Import Handler — Validation, Ownership, Error Surfacing
Fixes several gaps in the skill import flow:
1. **Skill validation now runs and surfaces properly.** The import
handler calls the real `createSkill(CreateSkillInput)` which runs
`validateSkillName`, `validateSkillDescription`, `validateSkillBody`.
Validation errors (SKILL_VALIDATION_FAILED) are caught and returned
as 400 with the issue messages. Duplicate-key errors return 409.
Previously all errors were swallowed into a generic 500.
2. **`authorName` is now populated.** The `CreateSkillInput` requires
`authorName` which was missing — resolved from `req.user.name ??
req.user.username ?? 'Unknown'`, matching the existing create handler.
3. **SKILL_OWNER permission is granted after import.** Calls
`grantPermission` with `AccessRoleIds.SKILL_OWNER` so the uploader
can edit/delete/share the imported skill. This was entirely missing —
imported skills would have been ownerless.
4. **`tenantId` propagated.** Both the skill and each SkillFile record
receive `req.user.tenantId` for multi-tenant deployments.
5. **SkillFile records are created in the DB.** Each non-SKILL.md file
in the zip is saved to file storage via `saveBuffer` and recorded
via `upsertSkillFile`, which validates the relativePath, infers the
category from the path prefix, and atomically bumps the skill's
`fileCount` and `version`.
Import deps now include `grantPermission` from PermissionService,
injected in `api/server/routes/skills.js`.
* 🐛 fix: Import grant uses accessRoleId (not roleId) — fixes skill not appearing in list
* 🎨 fix: Cache invalidation, file tree, frontmatter rendering
Three fixes for the skill detail view:
1. **Cache invalidation after import.** UploadSkillDialog now calls
`queryClient.invalidateQueries([QueryKeys.skills])` after a
successful import so the sidebar list picks up the new skill
without requiring a page refresh.
2. **File tree in detail view.** When a skill has `fileCount > 0`,
the detail view now queries `useListSkillFilesQuery` and renders
a file list below the body card — SKILL.md first, then folders
and root files. Icons: Folder for directories, FileText for files.
3. **Frontmatter stripped and rendered as metadata.** YAML frontmatter
(`---\nversion: 0.1.0\ntriggers: ...\n---`) is now parsed out of
the body before markdown rendering. The `name` and `description`
fields are skipped (already shown in the header). Remaining fields
(version, triggers, dependencies, etc.) are displayed in a
Claude.ai–style grid: label on the left, value on the right,
above the rendered markdown content. Source view still shows the
full raw body including frontmatter.
* 🩹 fix: Always fetch skill files — fileCount may be stale in cached skill object
* 🌳 feat: Inline File Tree in Sidebar Skill List
Moves the file tree from the bottom of SkillDetail into the sidebar
list, matching Claude.ai's pattern:
- Multi-file skills show a chevron toggle on the right side of the
skill list item
- Clicking the chevron expands an inline file tree below the skill
name: SKILL.md first, then folders (with folder icon + right
chevron) and root files
- File list is fetched lazily (only when expanded) via
useListSkillFilesQuery
- Clicking a file navigates to the skill detail view
- Files section removed from SkillDetail — the sidebar is now the
sole file tree location, keeping the detail panel clean
SkillDetail cleaned up: removed groupFiles helper, file-related
state, useListSkillFilesQuery import, FileText/Folder icon imports.
* 🌲 feat: Virtualized inline file tree with react-vtree
Replace hand-rolled recursive FolderRow/FileRow buttons with a proper
virtualized FixedSizeTree from react-vtree for the sidebar skill list.
Dynamic height tracks open folders; capped at 350px with smooth
expand/collapse transitions.
* chore: Remove no longer used SkillFileTree and SkillTreeNode components
* chore: Update Vite config to replace 'react-arborist' with 'react-vtree' for module resolution
* feat: Skill file content viewing with lazy DB caching
- Add `skills` field to `fileStrategiesSchema` so operators can
configure a dedicated storage backend for skill files. Falls back
by type (image/document) when unset.
- Fix hardcoded `FileSources.local` in skill save/import — now uses
the resolved strategy via `getFileStrategy(req.config, { context })`.
- Replace 501 download stub with real handler that streams from any
storage backend and returns JSON `{ content, mimeType, isBinary }`.
- Binary detection (null-byte + non-printable ratio on first 8 KB)
flags files on first read so they're never re-fetched.
- Text content ≤ 512 KB is cached in the SkillFile MongoDB document;
subsequent reads skip storage entirely.
- Clicking a skill row now expands inline files (not just chevron).
- Clicking a file navigates to `?file=<path>` and renders content
in a new SkillFileViewer (markdown, code, images, binary placeholder).
* chore: Remove react-window and its type definitions from package.json and package-lock.json
- Deleted `react-window` and `@types/react-window` dependencies from both `package.json` and `package-lock.json` to streamline the project and reduce unnecessary bloat.
* fix: Build errors — remove endpoints import, fix Uint8Array cast
- Replace `import { endpoints }` (not public) with inline URL in
SkillFileViewer
- Remove `as Uint8Array` cast in stream chunk handling
- Extend getSkillFileByPath return type with content/isBinary to
decouple from data-schemas build artifact resolution
* chore: Remove 8 unused i18next keys
com_ui_create_skill_ai, com_ui_create_skill_manual,
com_ui_delete_folder_confirm_var, com_ui_delete_skill,
com_ui_delete_skill_confirm_var, com_ui_delete_var,
com_ui_rename_var, com_ui_skill_files
* fix: Add configMiddleware to skills router, handle SKILL.md in viewer
- Add configMiddleware to skills router so req.config is populated
when getLocalFileStream (or any strategy) reads file paths.
- Handle SKILL.md in download handler — serves skill.body directly
from the Skill document instead of looking for a SkillFile record.
- Clicking SKILL.md in sidebar tree now opens the file viewer
(matching Claude.ai behavior: file view vs default detail view).
* ci: Run unit tests on PRs to any branch
Remove the branches filter from both test workflows so contributor
PRs targeting feature branches (not just main/dev) get CI coverage.
Path filters are kept so tests only run when relevant files change.
* fix: Update skills route tests for download handler changes
- Mock configMiddleware (sets req.config for file storage access)
- Mock getStrategyFunctions and getFileStrategy (storage strategy deps)
- Replace 501 stub test with SKILL.md content test + 404 test
* fix: Auto-expand files, frontmatter parsing, select-none, prefetch
- Auto-expand file tree when navigating directly to a skill URL
- Prefetch files for the active skill (eliminates first-expand lag)
- Fix frontmatter parser to handle multi-line YAML list values
(triggers field was missing because it uses list syntax)
- SkillFileViewer now parses frontmatter for .md files — shows
structured grid + rendered body (matching SkillDetail's display)
with source/rendered toggle
- Add select-none to all sidebar skill and file tree buttons
* refactor: Derive expanded state from isActive instead of useEffect
Replace useEffect sync with deterministic derivation:
expanded = hasFiles && (isActive || !collapsed)
Active skill is always open. collapsed is a manual toggle that
only takes effect on non-active items.
* fix: Remove empty space above body card — overlay view toggle
Move the rendered/source toggle from a dedicated row (40px of empty
space) to an absolute-positioned overlay in the card's top-right
corner, matching Claude.ai's layout.
* fix: Remove header bars from content editors — overlay action buttons
Collapse the full-width header bars ("Skill Content", "Text") in
SkillContentEditor, PromptTextCard, and PromptEditor. Action buttons
(edit/save toggle, copy, variables) are now absolute-positioned in
the card's top-right corner, reclaiming ~46px of vertical space.
* fix: Spinner visibility in file viewer — use text-text-secondary
* fix: Address review findings — security, correctness, code quality
Codex P1: Use $unset instead of undefined to clear cached content
and isBinary fields on file re-upload (Mongoose strips undefined).
Codex P2: Match skill-file validation errors by error.code instead
of error.message substring.
F1: Zip bomb defense — track cumulative decompressed bytes (500 MB
cap), check declared uncompressed size before buffering each entry.
F2: Remove misleading "atomically" from import handler JSDoc.
F3: Static import for isBinaryBuffer instead of dynamic import().
F4: Replace console.error with logger in upload handler.
F6: Add multer error handler middleware to skills router.
F7: Move React import to top of SkillDetail.tsx.
F9: Fix variable shadowing (trimmed → item) in parseFrontmatter.
F11: Replace JSON.parse(JSON.stringify()) with toJSON() for
Mongoose document serialization.
F12: Remove dead dynamic import('fs') fallback (memoryStorage
always provides file.buffer).
F13: Hoist MIME_MAP to module scope to avoid per-call allocation.
F16: Share single multer.memoryStorage() instance.
* fix: Follow-up review — close zip bomb gap, fix error handler
F1: Add post-decompression cumulative byte check with break (the
pre-decompression check relies on undocumented JSZip internals
that may be absent; this closes the gap unconditionally).
F2+F3: Multer error handler now forwards non-multer errors via
next(err) instead of swallowing them. Also catches file filter
rejections (plain Error, not MulterError) by message prefix.
F4: Move isBinaryBuffer import to local imports section per
CLAUDE.md import order rules.
F5: Simplify dead toJSON branch — createSkill returns a POJO.
* nit: Link filter error message to handler prefix check
* feat: Accordion expansion + active file highlight in sidebar
- Only one skill's file tree can be expanded at a time (accordion).
Expansion state lifted from SkillListItem to SkillList.
- Selected file gets bg-surface-active highlight in the tree.
Skill row uses subtle style (no background) when a file is active,
matching Claude.ai's pattern where the file — not the skill —
carries the selection state.
* style: Adjust margin for file tree in SkillListItem component
- Reduced left margin from 10 to 5 for improved layout consistency in the file tree display.
* fix: TS error on FileTreeNode, nested ternary, chevron collapse
- Make style prop optional to match react-vtree's NodeComponentProps
- Flatten nested ternary for skill row active styles
- Skill row click expands (but doesn't collapse) files + navigates
- Chevron click explicitly toggles collapse (matching Claude.ai
where clicking the chevron is how you collapse files)
* fix: Upload basePath, reject SKILL.md uploads, add skills permission route
- Pass basePath: 'uploads' in per-file upload handler (was defaulting
to 'images' path, inconsistent with the import flow).
- Reject uploads targeting SKILL.md (reserved path — download handler
special-cases it to return skill.body, making an uploaded file
unreachable via the API).
- Add skills entry to roles router permissionConfigs so PUT
/api/roles/:roleName/skills actually reaches a handler instead
of returning 404.
* feat: Expand content area, move controls to header, reduce padding
Default detail view:
- Remove rounded-xl bordered card wrapper — content flows directly
into the article, capitalizing on full screen width
- Move eye/code toggle inline with the divider row
- Reduce px-6/pb-6 to px-4/pb-4
File viewer:
- Move eye/code toggle from card overlay to the header bar
- Add copy-to-clipboard button for text files in the header bar
- Remove rounded-xl bordered card wrapper for markdown content
- Remove bordered pre wrapper for non-markdown text
- Reduce px-6/py-4 to px-4/py-3
Both views maximize content space over decorative chrome.
* fix: Stable header height, restore some padding
- Fix layout shift in file viewer header: use fixed h-10 so the
bar height stays constant whether the eye/code toggle renders
(markdown) or not (plain text).
- Bump content padding from px-4/py-3 back to px-5/py-4 in both
views — the previous reduction was too aggressive.
* fix: Grant rollback, path validation, error format, dead code cleanup
F2: grantOwnership now rolls back (compensating delete) on failure,
matching the create handler. Both markdown and zip import paths
check the result and return 500 on grant failure.
F4: Upload handler validates relativePath with regex + traversal
check before calling downstream upsertSkillFile.
F5: Document JSZip _data.uncompressedSize as best-effort; the
post-decompression cumulative check is the real safety net.
F10: Standardize all upload handler error responses to { error }
(was { message }, inconsistent with handlers.ts).
F13: Single-pass fileResults accumulation in import handler.
F1-5: Remove dead uploadFileStubHandler (no route references it).
Codex P2: Fix delete nav from /skills/new to /skills.
F12: Use cn() in UploadSkillDialog instead of template literals.
* perf: Stream-first binary detection + O(1) public skill check
F1: Download handler now reads only the first 8 KB for binary
detection. If binary, the stream is destroyed immediately without
buffering the remaining file. Text files continue reading for
caching. Eliminates buffering up to 10 MB per request for binary
files under concurrent load.
F7: Single-skill GET and PATCH now use hasPublicPermission (O(1)
ACL lookup) instead of getPublicSkillIdSet (queries ALL public
skill IDs). The list handler still uses the Set approach since it
serializes multiple skills. serializeSkill/serializeSkillSummary
now accept boolean | Set for flexibility.
* fix: Update test to match { error } response format
* fix: Critical stream truncation bug, grantedBy, error format
NF-1 (CRITICAL): Rewrite binary detection to single for-await loop.
Breaking out of for-await-of destroys the stream via iterator.return(),
so the previous two-loop approach silently truncated text files > 8KB.
Now: one loop collects chunks, checks binary after 8KB accumulated,
and either destroys+returns (binary) or continues reading (text).
NF-2: Add grantedBy to import handler's grantPermission call and
interface (was missing, inconsistent with create handler).
NF-3: Standardize all import handler error responses from { message }
to { error }, matching handlers.ts convention. Update client's
UploadSkillDialog to read response.data.error accordingly.
* fix: Prefer specific validation message over generic error field
* fix: YAML quote stripping, saveBuffer null guard, dot segment rejection
- Strip surrounding YAML quotes from frontmatter values so
name: "my-skill" parses as my-skill (not "my-skill" with quotes
that fails the name validator).
- Guard resolveSkillStorage against backends with saveBuffer: null
(e.g. OpenAI/vector strategies) — throws a descriptive error
caught by the handler's try/catch instead of a TypeError.
- Tighten upload path validation to reject . segments (e.g.
docs/./a.md) matching the model-layer validator, preventing
storage writes for paths the DB will reject.
* fix: Orphan cleanup, stream errors, malformed zip, cache latency
F1: Upload handler now deletes the stored blob if the subsequent
DB upsert fails, preventing orphaned files on disk/cloud.
F2: Multer error handler returns { error } (was { message }).
F3: Wrap JSZip.loadAsync in try/catch — malformed zip returns 400
instead of falling through to 500.
F4: Raw download stream gets an error handler — logs the error and
destroys the response if headers were already sent.
F8: Strip leading hyphens from inferred skill name so filenames
like _my-skill.zip don't produce -my-skill (invalid name pattern).
F9: Fire-and-forget all updateSkillFileContent cache writes so the
response is sent immediately. Cache failures are logged but don't
block or fail the read.
* fix: Import orphan cleanup + Content-Disposition sanitization
Finding A: Add deleteFile dep to ImportSkillDeps. The per-file loop
in handleZip now cleans up stored blobs when upsertSkillFile fails,
closing the second half of the F1 orphan fix (upload handler was
already fixed).
Finding B: Sanitize filename in Content-Disposition header for raw
downloads — strip quotes, backslashes, and newlines to prevent
header injection from user-uploaded filenames.
* security: Prevent stored XSS via raw file downloads
Non-image files served via ?raw=true now use Content-Disposition:
attachment (force download) instead of inline. An uploaded .html or
.svg file served inline from the LibreChat origin could execute
scripts with access to the user's session — this closes that vector.
Images stay inline (needed for <img> rendering in SkillFileViewer).
X-Content-Type-Options: nosniff added to prevent MIME sniffing.
* security: Block SVG XSS — allowlist safe raster MIME types for inline
SVG (image/svg+xml) passed the startsWith('image/') check and was
served inline, but SVG is a scriptable format — embedded <script>
tags execute in the LibreChat origin. Replace the prefix match with
a Set of safe raster-only MIME types (png, jpeg, gif, webp, avif,
bmp). SVGs and any future scriptable image/* subtypes now get
Content-Disposition: attachment (forced download).
* fix: Cap JSON text response at 1MB, consistent md name inference
F3: Text files > 1MB now return { isBinary: false } with no content
field, forcing the client to use ?raw=true for download. Prevents
buffering 10MB files into heap for JSON serialization. Frontend
shows a download fallback when content is absent.
F4: handleMarkdown now infers skill name from filename (same as
handleZip) when frontmatter has no name, instead of rejecting
with 400. Consistent behavior across import paths.
F1 (reviewer concern): upsertSkillFile is NOT affected — it uses
{ new: false } for insert-vs-replace detection but does a follow-up
findOne (lines 855-859) to return the post-upsert document.
* fix: deleteFile arg shape, raw URL base path, hoist SAFE_INLINE_MIMES
Codex P2: deleteFile expects { filepath } object, not a raw string.
Both upload handler cleanup and import handler cleanup now pass
{ filepath } to match the strategy contract (deleteLocalFile,
deleteFileFromS3 all expect a file object).
Codex P2: Raw download URL in SkillFileViewer now uses apiBaseUrl
prefix so subpath deployments (/chat, etc.) resolve correctly.
NIT: Hoist SAFE_INLINE_MIMES Set to factory scope — was re-allocated
per raw download request inside the if block.
* fix: Remove inert cache write for large text files, localize aria-label
N2: The { isBinary: false } cache write for text files > 1MB had no
effect — subsequent requests still fell through to stream read since
neither isBinary nor content provided a fast-path short-circuit.
Removed the pointless DB updateOne per request.
N4: Replace hardcoded "Back to skill" aria-label with localize().
* refactor: Extract shared parseFrontmatter, widen deleteFile type
N3: Extract parseFrontmatter into Skills/utils/frontmatter.ts —
single implementation shared by SkillDetail and SkillFileViewer.
Accepts optional skipKeys set so callers control which frontmatter
fields are excluded (SkillDetail skips name/description since
they're shown in the header; other .md files show all fields).
N5: Widen ImportSkillDeps.deleteFile file param from { filepath }
to { filepath; [key: string]: unknown } to signal extensibility
if strategies start accessing additional file properties.
* fix: Advance i past list items for skipped keys, DRY parseSkillMd
Finding A: parseFrontmatter now consumes multi-line YAML list items
before checking skipKeys — prevents list lines from leaking into
subsequent key parsing as spurious fields.
Finding B: parseSkillMd now delegates to the shared parseFrontmatter
instead of re-implementing the same frontmatter scanning loop.
Reduces client-side parseFrontmatter implementations from 3 to 1.
* fix: Call apiBaseUrl(), delete storage blob on file removal
- apiBaseUrl is a function, not a string — call it in the template
literal so raw download URLs resolve correctly.
- deleteFileHandler now looks up the file record before deleting,
then fire-and-forget deletes the storage blob via the strategy's
deleteFile. Previously only the DB record was removed, leaving
orphaned blobs in local/S3/Firebase/Azure storage.
* fix: Clean up storage blobs when deleting an entire skill
deleteHandler now lists all files for the skill before calling
deleteSkill, then fire-and-forget deletes each blob via the
storage strategy. Previously only per-file deletion cleaned up
blobs — deleting a whole skill left all associated files orphaned
in local/S3/Firebase/Azure storage.
* refactor: useImportSkillMutation hook, fix TSkill[] unsafe cast
- Create useImportSkillMutation in mutations.ts + ImportSkillOptions
type. UploadSkillDialog now uses the mutation hook instead of
calling dataService.importSkill directly with manual useState
loading management. Eliminates unmounted-component state update
risk and aligns with the React Query mutation pattern used by
every other mutation in the codebase.
- SkillSelectDialog: replace as unknown as TSkill[] with proper
TSkillSummary typing. SkillCard props updated to TSkillSummary.
The dialog only uses summary-level fields (name, description,
category, author) — the cast was hiding a type mismatch.
* fix: Use saved source for import cleanup, delete old blob on replace
Codex P2: Import cleanup now uses file.source (the backend the file
was actually saved to) instead of re-resolving from config. In mixed
strategy setups, the previous approach could target the wrong backend.
Codex P2: When re-uploading a file to an existing relativePath, the
old blob is now deleted after successful upsert. Previously only the
DB record was replaced, leaving the old storage object orphaned.
* fix: Register PUT /:roleName/skills route in roles router
* fix: Re-read skill after zip file processing for fresh metadata
The import response was built from the skill object created before
the file loop, but each upsertSkillFile bumps version and fileCount.
Clients caching the stale response would get 409 conflicts on first
edit and see incorrect file counts.
Now re-reads the skill via getSkillById after the loop so the
response reflects the current version, fileCount, and updatedAt.
* fix: Size-check SKILL.md before decompression, don't gate on fileCount
P1: SKILL.md was decompressed before any size accounting. A crafted
archive could expand SKILL.md past 10MB before validation ran. Now
checks declared size pre-decompression and actual size post, both
against MAX_SINGLE_FILE_BYTES.
P2: File list query was gated on cached fileCount which can be stale
after mutations. Now fetches files for the active skill regardless
of fileCount. hasFiles derived from fetched data with fileCount as
fallback, so newly uploaded files appear without hard refresh.
* fix: Move files declaration before hasFiles to avoid TDZ error
* security: Stream-decompress zip entries with enforced byte cap
Replace zipEntry.async('nodebuffer') (buffers entire entry before
checking limits) with zipEntry.nodeStream('nodebuffer') piped
through a byte counter that destroys the stream when the per-file
or cumulative limit is exceeded.
Previously, when JSZip's _data.uncompressedSize was absent (the
common case), a high-ratio entry could allocate hundreds of MB
before the post-decompression check caught it. Now decompression
is aborted mid-stream at the exact byte threshold — no entry can
exceed its limit regardless of compression ratio.
* refactor: Reorganize access check for prompts in useSideNavLinks hook
Moved the prompts access check to a new position in the code to improve readability and maintainability. This change ensures that the prompts link is added to the navigation only if the user has the appropriate access, without altering the existing functionality.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🧬 feat: Scaffold Skills CRUD with ACL Sharing and File Schema
Adds Skills as a new first-class resource modeled on Anthropic's Agent
Skills, reusing the existing Prompt ACL stack for sharing. Lays the
groundwork for multi-file skills (SkillFile schema + metadata routes)
without wiring upload processing — single-file skills (inline SKILL.md
body) work end-to-end, multi-file uploads are stubbed for phase 2.
* 🔬 fix: Wire Skill Cleanup, AccessRole Enum, and Express 5 Path Params
CI surfaced four follow-ups from the initial Skills scaffolding commit
that local builds missed:
- AccessRole's resourceType field had a hardcoded enum that didn't
include `'skill'`, blocking SKILL_OWNER/EDITOR/VIEWER role creation
in every test that hit the AccessRole model.
- The seedDefaultRoles assertion in accessRole.spec.ts hard-listed the
expected role IDs and needed the new SKILL_* entries.
- deleteUserController had no cleanup for skills, and the
deleteUserResourceCoverage guard test enforces every ResourceType has
a documented handler — wired in db.deleteUserSkills(user._id) and
added the entry to HANDLED_RESOURCE_TYPES.
- Express 5's path-to-regexp v6 rejects the legacy `(*)` named-group
glob syntax. The two skill file routes now use a plain `:relativePath`
param; the client already encodeURIComponents the path, so a single
param is sufficient and decoded server-side.
* 🪡 fix: Make Skill Name Uniqueness Application-Level
Resolve three more CI failures from the Skills scaffolding PR:
- Mongoose creates indexes asynchronously and mongodb-memory-server
tests can race ahead of the unique (name, author, tenantId) index
being built, so the duplicate-name uniqueness test was flaky.
Added an explicit findOne pre-check inside createSkill that throws
with code 11000 (mimicking the index violation), giving deterministic
behavior. The unique index stays as the persistent guarantee.
- The deleteUser.spec.js and UserController.spec.js suites mock the
~/models module directly and were missing deleteUserSkills, causing
deleteUserController to throw and return 500 instead of 200.
- Removed two doc-comment claims that the SKILL_NAME_MAX_LENGTH and
SKILL_DESCRIPTION_MAX_LENGTH constants "match Anthropic's API". The
values themselves are reasonable but the comments were misleading
about who enforces them.
* 🪢 fix: Address Code Review Findings on Skills Scaffolding
Resolve all 15 findings from the comprehensive PR review:
Critical:
- Rollback the created skill when grantPermission throws so a transient
ACL failure cannot leave an orphaned, inaccessible skill in the DB.
- Fix infinite query cache corruption in useUpdateSkillMutation helpers.
setQueriesData([QueryKeys.skills]) matches useSkillsInfiniteQuery's
InfiniteData cache entries, which have { pages, pageParams } shape —
spreading data.skills on those would throw. Added an isInfiniteSkillData
guard and per-page transform so both flat and infinite caches update
correctly.
Major:
- Fix TUpdateSkillContext type: the public type declared previousListData
but onMutate actually returns previousListSnapshots (a [key, value]
tuple array). Updated the type + added TSkillCacheEntry as a shared
export from data-provider.
- Add cancelQueries calls before optimistic update in onMutate so
in-flight refetches cannot clobber the optimistic state.
- Parallelize deleteUserSkills ACL removal via Promise.allSettled instead
of a sequential await loop — O(1) round-trip vs O(n).
- Stub mockDeleteUserSkills in stubDeletionMocks() and assert it's called
with user.id in the deleteUser.spec.js happy-path test.
- Add idResolver: getSkillById to the SKILL branch in accessPermissions.js
so GET /api/permissions/skill/<missing-id> returns 404 instead of 403.
Minor:
- Reuse resolved skill from req.resourceAccess.resourceInfo in getHandler
to eliminate a redundant getSkillById call per GET /api/skills/:id.
- Reject PATCH /api/skills/:id requests whose body contains only
expectedVersion — previously they silently bumped version with no
changes, triggering spurious 409s for collaborators.
- Make TSkill.frontmatter optional (wire type) and add serializeFrontmatter
/ serializeSourceMetadata helpers that return undefined for empty
objects instead of casting incomplete data to SkillFrontmatter.
- Standardize deleteUserSkills to accept string | ObjectId and convert
internally, matching deleteUserPrompts's signature; UserController now
passes user.id consistently.
- Replace bumpSkillVersionAndRecount (read-then-write, racy) with
bumpSkillVersionAndAdjustFileCount using atomic $inc. upsertSkillFile
pre-checks existence to distinguish insert (+1) from replace (0).
- Add DELETE /api/skills/:id/files/:relativePath integration tests
covering success, 404, and 403 paths.
Nits:
- Drop trivial resolveSkillId wrapper — pass getSkillById directly.
- Remove dead staleTime: 1000 * 10 from useListSkillsQuery since all
refetch triggers are already disabled.
* 🧭 fix: Resolve Second Skills Review Pass — Cache, Gate, TOCTOU
Address 13 of 14 findings from the second code review; reject #13 as
misread of the AGENTS.md import-order rule (package types correctly
precede local types regardless of length).
Major:
- Fix addSkillToCachedLists closure bug: a hoisted `prepended` flag
was shared across every cache entry matched by setQueriesData, so
concurrent flat + infinite caches would silently drop the prepend
on whichever was processed second. Replaced the shared helper with
three per-entry inline updaters that handle InfiniteData at the
page level (page 0 only for prepend, all pages for replace/remove).
- Tighten patchHandler's expectedVersion validation: NaN passes
`typeof === 'number'` and would previously leak current skill state
via a misleading 409. Now requires finite positive integer and
returns 400 otherwise.
- Guard decodeURIComponent in deleteFileHandler with try/catch —
malformed percent encoding now returns 400 instead of 500.
- Add PermissionTypes.SKILLS + skillPermissionsSchema +
TSkillPermissions in data-provider; seed default SKILLS permissions
for ADMIN (all true) and USER (use + create only); wire
checkSkillAccess / checkSkillCreate via generateCheckAccess onto
the skills router mirroring the prompts pattern. Skills route now
enforces role-based capability gates alongside per-resource ACLs.
Test suite adds a mocked getRoleByName returning permissive SKILLS.
- Fix upsertSkillFile TOCTOU: replaced the pre-check + upsert pair
with a single `findOneAndUpdate({ new: false, upsert: true })` call
that atomically returns the pre-update doc (null ⇒ insert) so
fileCount delta can't double-count on concurrent same-path uploads.
Minor:
- Add `sourceMetadata` to listSkillsByAccess .select() so summaries
no longer silently drop the field for GitHub/Notion-synced skills.
- Include `cursor` in useListSkillsQuery's query key so manual
pagination doesn't alias across pages.
- Clean up TSkillSummary to `Omit<TSkill, 'body' | 'frontmatter'>`
matching what serializeSkillSummary actually emits; drop the
Omit-then-re-add noise.
- Skip getPublicSkillIdSet in createHandler; a newly-created skill
cannot have a PUBLIC ACL entry, so pass an empty set directly
instead of paying a DB round-trip.
- Trim SkillMethods public surface: drop internal helpers
countSkillFiles / deleteSkillFilesBySkillId / getSkillFile from the
return object; inline the file cascade into deleteSkill.
- Use TSkillConflictResponse at the PATCH 409 call site instead of
an inline ad-hoc object literal.
- Drop the now-unused EXPECTED_VERSION_ERROR module constant.
* 🧩 fix: Extend Role Schema + Types with SKILLS PermissionType
CI type-check and unit test failures from the PermissionTypes.SKILLS
addition surfaced three unrelated places that all hardcode the
permission-type set:
- IRole.permissions in data-schemas/types/role.ts enumerates every
PermissionTypes key as an optional field. Adding SKILLS to the enum
without updating the interface caused TS7053 'expression of type
PermissionTypes can't be used to index type' errors in
role.methods.spec.ts (lines 407-408, 477-478) because
Object.values(PermissionTypes) now yielded a value the interface
didn't cover.
- schema/role.ts rolePermissionsSchema mirrors the interface at the
Mongoose layer; also needed SKILLS added so the persisted role
document can actually store skill permissions.
- data-provider/roles.spec.ts has a guard test that every permission
type carrying CREATE/SHARE/SHARE_PUBLIC must be explicitly "tracked"
either in RESOURCE_PERMISSION_TYPES or in the PROMPTS/AGENTS/MEMORIES
exemption list. Added SKILLS to the exemption list since skills
follow the same default model as prompts/agents (USE + CREATE on for
USER, SHARE / SHARE_PUBLIC off).
All three are additive pass-throughs with no behavior change.
* 🏷️ refactor: Introduce ISkillSummary for Narrow List Projection
Follow-up NITs from the second review pass on the Skills PR:
- Define ISkillSummary = Omit<ISkill, 'body' | 'frontmatter'> and use
it as the element type in ListSkillsByAccessResult. The list query's
.select() intentionally omits body and frontmatter for payload size,
but the previous type claimed both fields were present — a type lie
that would mislead future readers even though serializeSkillSummary
never touches those fields at runtime. handlers.ts's signature for
serializeSkillSummary now accepts ISkillSummary too.
- Document the intentional second-round-trip `findOne` in
upsertSkillFile. Switching to `findOneAndUpdate({ new: false })`
was required for TOCTOU-safe insert-vs-replace detection, which
means the handler needs a follow-up query to return the post-upsert
document. A comment now explains the tradeoff so future readers
don't silently "optimize" it away.
No behavior change.
* 🌐 fix: Wire SKILL into SHARE_PUBLIC Resource Maps
Address codex comment #1 — making a skill public was blocked on two
hardcoded resource→permission-type maps that didn't know about SKILL:
- api/server/middleware/checkSharePublicAccess.js's
resourceToPermissionType map was missing ResourceType.SKILL, so
PUT /api/permissions/skill/:id with { public: true } would fall
through to the 400 "Unsupported resource type for public sharing"
path even though PermissionTypes.SKILLS exists and ADMIN has
SHARE_PUBLIC configured. Added the mapping.
- client/src/hooks/Sharing/useCanSharePublic.ts has an identical
client-side map used to gate the "Make Public" UI toggle. Without
the SKILL mapping the hook returned false for everyone, so the
toggle wouldn't render for skills once the sharing UI lands in
phase 2. Added the mapping.
Codex comment #2 (create/update cache writes inject skills into
unrelated filtered lists) is invalid — it flags a pattern that
mirrors useUpdatePromptGroup (which the PR description explicitly
cites as the model) and is a deliberate optimistic-update tradeoff.
Trying to match each cache key's embedded filter would couple the
mutation callback to query-key internals, which is exactly what
setQueriesData is designed to avoid. No change there.
* 🧪 feat: Frontmatter Validation, Reserved-Name Fixes, Coaching Warnings
Address the follow-up review notes on the Skills PR. This commit closes
the gap between the wire-type promise and what the backend actually
enforces, tightens the reserved-name rules, and adds a non-blocking
coaching tier for validators.
Frontmatter validation (new):
- Add `validateSkillFrontmatter` in data-schemas/methods/skill.ts with
strict mode — unknown keys are rejected so expanding the allowed set
is an intentional code change. Known keys are type-checked against a
`FrontmatterKind` table derived from Anthropic's Agent Skills spec
(name, description, when-to-use, allowed-tools, arguments,
argument-hint, user-invocable, disable-model-invocation, model,
effort, context, agent, paths, shell, hooks, version, metadata).
- `hooks` and `metadata` get a shallow JSON-safety check (max depth 4,
max string 2000, max array 100) instead of a full schema, since their
full shapes live outside this module.
- Wired into BOTH createSkill AND updateSkill so the PATCH path can't
smuggle invalid frontmatter past the validator.
Validation warning tier (new):
- Add optional `severity: 'error' | 'warning'` to `ValidationIssue`
(defaults to error). `partitionIssues` splits an issue list into
blocking errors and non-blocking warnings.
- `createSkill` / `updateSkill` filter on errors for the throw check
and return warnings in a new `warnings: ValidationIssue[]` field on
their result objects (`CreateSkillResult` / `UpdateSkillResult`).
- `validateSkillDescription` now emits a `TOO_SHORT` warning for
descriptions under 20 chars — the primary triggering field, so a
little coaching goes a long way.
- `createHandler` / `patchHandler` in packages/api surface the warnings
via a new `attachWarnings` helper that decorates the serialized
response with a `warnings?: TSkillWarning[]` field.
- `TSkill` gains an optional `warnings?: TSkillWarning[]` field
documented as "present on POST/PATCH, never on GET".
Reserved-name filter (tightened):
- Replace the substring match (`.includes('anthropic')`) with prefix
matching on `anthropic-` and `claude-` plus exact-match rejection of
CLI slash-command collisions (help, clear, compact, model, exit,
quit, settings, plus the bare `anthropic` / `claude` words). Both
the pure validator (`methods/skill.ts`) and the Mongoose schema
validator (`schema/skill.ts`) updated in lockstep; comments on
each reference the other to prevent drift.
- `research-anthropic-helper` and `about-claude` are now allowed;
`anthropic-helper`, `claude-bot`, and `settings` are still rejected.
Documentation:
- Add docstrings on `ISkill`, `schema/skill.ts`, and `TSkill` explaining
the semantics of `name` (Claude-visible identifier, kebab-case,
stable), `displayTitle` (UI-only cosmetic label, NOT sent to Claude),
`description` (highest-leverage trigger field), and `source` /
`sourceMetadata` (reserved for phase 2+ external sync).
- Add a detailed consistency comment on `bumpSkillVersionAndAdjustFileCount`
explaining that it runs as a separate MongoDB operation from
upsertSkillFile/deleteSkillFile, so `fileCount` can drift if the
second op fails — options listed, tradeoff documented, phase 1
risk window noted as closed because upload is still stubbed.
Tests:
- data-schemas skill.spec.ts: destructure `{ skill, warnings }` from
createSkill at every call site; add a TOO_SHORT warning test, a
frontmatter strict-mode test, reserved-prefix tests (including
positive cases for substring names that should pass), CLI reserved
word tests, and a full `validateSkillFrontmatter` describe block
covering unknown keys, type mismatches, and deep-nesting rejection.
- api/server/routes/skills.test.js: bump default test description
above the 20-char threshold, add a warning-emission test, add
reserved-prefix + reserved-CLI-word tests, add an unknown-frontmatter-
key test asserting the 400 response carries `issues` with `UNKNOWN_KEY`.
* 📦 fix: Export CreateSkillResult from data-schemas Methods Index
`CreateSkillResult` was defined in `methods/skill.ts` and consumed by
`packages/api/src/skills/handlers.ts` but never re-exported from the
methods barrel, so the type-check job failed with TS2724
"'@librechat/data-schemas' has no exported member named 'CreateSkillResult'".
Rollup's bundle-mode build picked up the type via its internal resolver,
but the standalone `tsc --noEmit` type-check ran against the package's
public entrypoint and couldn't see it. Added the type import + export
alongside the existing `UpdateSkillResult` export, which fixes the
CI type-check without any runtime change.
* 🛡️ fix: Install global `unhandledRejection` handler
Node 15+ terminates the process by default when a promise rejection goes
unhandled. Under MCP OAuth reconnect storms and streamable-HTTP transport
resets, fire-and-forget async paths can emit transient rejections (ECONNRESET,
token refresh races) that would otherwise silently kill the server — no
uncaught exception log, no OOM signal. Register a listener so these paths log
and the process keeps serving other requests.
Refs: #12078
* 🔧 fix: Guard MCP OAuth reconnect fire-and-forget calls
`OAuthReconnectionManager.tryReconnect` awaits `getServerConfig` outside its
inner try/catch, so a rejection from the registry (or any throw before the
guarded block) would escape the fire-and-forget `void` call sites and
propagate as an unhandled rejection — the failure mode behind the silent
crashes reported in #12078. Route both call sites through a `safeTryReconnect`
wrapper that attaches a terminal `.catch` so unexpected rejections are
surfaced via the logger instead.
Refs: #12078
* 🧹 fix: Address review findings on MCP OAuth reconnect crash fix
- Move `getServerConfig` inside `tryReconnect`'s try/catch so the registry
rejection path is handled by the inner cleanup (the structural root cause
behind the silent crash). The outer `safeTryReconnect` wrapper remains as
defense-in-depth.
- Extract the failed-reconnect cleanup as a private `cleanupOnFailedReconnect`
method and invoke it from `safeTryReconnect`'s catch as well, so any
rejection that does escape the inner try (e.g. a future regression) still
resets tracker state instead of leaving the server stuck in `active` for
the full `RECONNECTION_TIMEOUT_MS` window.
- Update the regression test to assert tracker state is cleaned up
(`isActive` cleared, `isFailed` set, `disconnectUserConnection` called) so
it can detect the stale-state failure mode it was meant to guard against.
- Forward non-Error rejection reasons as-is in the global handler so
structured payloads like `{ code: "ECONNRESET", errno: -104 }` survive
instead of being collapsed to "[object Object]" by `String()`.
Refs: #12078, review of #12812
* 🚑 fix: Restore fail-fast on boot rejection in primary server entry
`startServer()` was invoked bare in `api/server/index.js`. Before installing
the global `unhandledRejection` handler, a startup rejection (`connectDb`,
`getAppConfig`, `performStartupChecks`) terminated the process via Node's
default — Kubernetes / the orchestrator restarted the pod immediately.
After the handler was added, the same rejection was caught and logged, then
the process kept running half-initialized (no HTTP listener) until the
liveness probe eventually timed out — slow, indirect recovery instead of a
fast restart.
Wrap `startServer()` with the same `.catch(() => process.exit(1))` pattern
already used in `experimental.js` so boot failures fail-fast.
Refs: #12078, codex review of #12812
* 🚑 fix: Fail-fast on post-listen init failure in both server entries
The `app.listen` callback in `index.js` and `experimental.js` is async and
awaits `initializeMCPs`, `initializeOAuthReconnectManager`, and
`checkMigrations`. The callback's promise is detached from
`startServer().catch(...)` (the outer catch only sees errors that occurred
before `app.listen` was called), so without explicit handling those init
rejections used to terminate the process via Node's default and now would
be swallowed by the new `unhandledRejection` handler — leaving the HTTP
server listening (and passing liveness probes) while MCP / OAuth / migration
state is broken.
Wrap the post-listen init block in a try/catch that logs and calls
`process.exit(1)` so initialization failures stay fail-fast.
Refs: #12078, codex review of #12812
`tModelSpecPresetSchema` was omitting `greeting` and `iconURL`, so
`configSchema.strict().safeParse()` stripped these admin-configured
fields from `modelSpecs.list[].preset` before the server sent the
startup config to the client — breaking the landing greeting and the
`preset.iconURL` fallback in `getModelSpecIconURL`.
Keep `spec` and `presetOverride` omitted (those are truly
client-managed), and flip the schema test to assert `greeting`/`iconURL`
are preserved.
Fixes#12803
Deletes seven inherited workflows that published unused images
(chat-api, lc-dev*, lc-dev-staging*, LiteLLM ghcr_deploy/helm chart) —
none are referenced by hanzoai/universe manifests, all used QEMU-emulated
builds instead of ARC runners.
docker-publish.yml now builds linux/amd64 + linux/arm64 via the canonical
reusable. One workflow, one way, multi-arch by default.
* ci: remove upstream LibreChat CI cruft; enable arm64 in canonical
Deletes seven inherited workflows that published unused images
(chat-api, lc-dev*, lc-dev-staging*, LiteLLM ghcr_deploy/helm chart) —
none are referenced by hanzoai/universe manifests, all used QEMU-emulated
builds instead of ARC runners.
docker-publish.yml now builds linux/amd64 + linux/arm64 via the canonical
reusable. One workflow, one way, multi-arch by default.
* ci: migrate to canonical hanzoai/.github/docker-build.yml reusable
Deletes seven inherited workflows that published unused images
(chat-api, lc-dev*, lc-dev-staging*, LiteLLM ghcr_deploy/helm chart) —
none are referenced by hanzoai/universe manifests, all used QEMU-emulated
builds instead of ARC runners.
docker-publish.yml now builds linux/amd64 + linux/arm64 via the canonical
reusable. One workflow, one way, multi-arch by default.
Addresses Dependabot advisory (uncontrolled recursion in XML serialization, DoS).
All transitive parents — mammoth, @node-saml/node-saml, xml-crypto, xml-encryption —
already accept `^0.8.x`, so this is a patch-level bump with no breaking changes.
- Enabled `esModuleInterop` in `client/tsconfig.json` for better module compatibility.
- Changed `moduleResolution` from `node` to `bundler` in `client/tsconfig.json`.
- Set `noEmit` to `true` in several `tsconfig.json` files to prevent output generation.
- Removed `baseUrl` from various `tsconfig.json` files to simplify path resolution.
- Updated path mappings in multiple packages to reflect new directory structures.
These changes aim to streamline TypeScript configurations and improve module resolution across the project.
* fix: restore tenant context in MCP OAuth callback for multi-tenant deployments
The MCP OAuth callback is a cross-origin redirect from the OAuth
provider. SameSite=Strict cookies (including the JWT) are not sent,
leaving the callback with no tenant context. With
TENANT_ISOLATION_STRICT=true, all DB writes fail.
Stores tenantId in flow metadata at OAuth initiation time (when
the user is authenticated), then restores it via tenantStorage.run
in the callback, wrapping the entire post-validation body.
* test: address review findings for tenant context tests
- Assert tenantId flows through to initFlow in MCPConnectionFactory test
- Add beforeEach to tenant context tests to reset mocks independently
* 🧹 fix: Prune Orphaned File References on File Deletion
Deleting a file via the Manage Files tab left its file_id in every agent's
tool_resources.*.file_ids. Stubs accumulate until the frontend dedupe keys
them as duplicates and blocks all new uploads (issue #12776).
- Add removeAgentResourceFilesFromAllAgents in packages/data-schemas: a
single updateMany/$pullAll across every EToolResources category.
- Invoke it from processDeleteRequest after db.deleteFiles so every
referencing agent is cleaned up, not just the one passed in req.body.
- Wrap the cleanup in try/catch so a stale agent update cannot mask a
successful file deletion.
* 🧼 fix: Prune Orphaned File References on Agent Update
Already-affected agents would stay broken even after the delete-time fix:
the stubs sit on the agent document until something strips them. Heal them
on the next save (issue #12776).
- Add collectToolResourceFileIds + stripFileIdsFromToolResources helpers
in @librechat/api — centralizing the tool_resources traversal used by
the controller and the follow-up migration script.
- In updateAgentHandler, check the effective tool_resources against the
files collection. When orphans are found, either strip them from the
incoming tool_resources (if the update sets them) or run the bulk
cleanup (if the update leaves tool_resources untouched).
* 🧰 chore: Add Migration to Clean Up Orphaned Agent File References
Complements the delete-time and save-time fixes by healing agents that
already accumulated orphan stubs before the upgrade (issue #12776). The
script is idempotent — re-running it on a clean database is a no-op.
- Add config/migrate-orphaned-agent-files.js following the existing
migrate-*.js convention: --dry-run by default omitted (writes by
default) and --batch-size= tuning knob. Streams agents via cursor.
- Register migrate:orphaned-agent-files and :dry-run npm scripts.
- Reuse collectToolResourceFileIds from @librechat/api so migration and
runtime share the same traversal logic.
* 🩹 fix: Address Codex/Copilot Review on Orphaned Agent File Cleanup
Refines the #12776 fix series based on automated review feedback.
- Scope save-time pruning to the current agent only. When a PATCH
carries tool_resources, strip orphans from the incoming payload and
pay the DB round-trip only then. Removes the collection-wide
updateMany previously triggered when tool_resources was absent
(Codex P2 / Copilot).
- Wrap the orphan check in try/catch so a transient db.getFiles
failure can't turn a good save into a 500 (comprehensive review #1).
- Replace Object.values(EToolResources) casts with an explicit list of
agent-side categories in both orphans.ts and agent.ts. code_interpreter
belongs to the Assistants API and isn't a key of AgentToolResources —
including it was a type lie and generated dead MongoDB clauses
(comprehensive review #3, #8).
- Export TOOL_RESOURCE_KEYS from @librechat/api and consume it in the
migration script, dropping one duplicated definition (#4).
- Cap migration results.details at 50 sample entries so the memory
footprint stays bounded on deployments with thousands of corrupted
agents (Codex P3).
- Add migrate:orphaned-agent-files:batch npm script to match the
convention set by migrate-agent-permissions / migrate-prompt-permissions
(#7).
- Add controller-level tests covering the three orphan-pruning paths:
strip from incoming tool_resources, leave alone when tool_resources
is absent, swallow db.getFiles errors and still save (#6).
- Back pre-existing "should validate tool_resources in updates" test's
file_ids with real File docs — the new pruning would otherwise strip
them, and that test is about OCR conversion / schema filtering, not
file existence. Register the File model in beforeAll so the fixture
works.
* 🩹 fix: Tighten TOOL_RESOURCE_KEYS Type and Align Migration Sample Output
Two follow-ups from the second review pass.
- Type data-schemas' TOOL_RESOURCE_KEYS as ReadonlyArray<keyof
AgentToolResources> instead of readonly string[]. Data-schemas depends
on data-provider, so the import is clean. Catches typos and aligns
with the matching export in @librechat/api — doesn't guarantee
exhaustiveness, but that's a TypeScript limitation, not a workspace
one.
- Align the migration's console output with DETAIL_SAMPLE_LIMIT: print
every collected detail (up to 50) and, when more agents were affected
than the sample size allowed, show a truncation notice. The old hard
cap of 25 meant affected agents in the 26-50 range were collected
but never shown.
* ✅ test: Add Integration Coverage for Orphan Cleanup Paths (#12776)
Exercise the delete-time and migration paths end-to-end against a real
in-memory Mongo. Catches integration bugs the isolated unit tests on
each layer couldn't.
- api/server/services/Files/process.integration.spec.js — the primary
repro: seed an Agent + File, call processDeleteRequest, assert the
file_id disappears from every referencing agent's tool_resources
while unrelated agents stay untouched. Also covers the no-op case
and confirms a failure in the new cleanup step cannot roll back the
file deletion itself.
- api/test/migrate-orphaned-agent-files.spec.js — drives the migration
module: --dry-run reports without writing, apply mode prunes across
every tool_resource category, re-running is idempotent, and
DETAIL_SAMPLE_LIMIT caps the in-memory sample on wide corruption.
Mocks only the connect helper (the spec owns the mongoose instance)
so the real migration code path — cursor, $pullAll, reduce — runs.
* 🔒 fix: Run Orphan Cleanup Migration in System Tenant Context
Codex P2 catch: under TENANT_ISOLATION_STRICT=true, the migration
throws on the very first Agent.countDocuments() because the tenant
isolation plugin fail-closes on queries without tenant context — which
makes migrate:orphaned-agent-files unusable on the exact deployments
most likely to have accumulated corruption.
- Wrap the scan/prune body in runAsSystem so queries bypass the tenant
filter (SYSTEM_TENANT_ID sentinel). The migration legitimately needs
cross-tenant visibility — this is the same pattern seedDatabase and
the S3 refresh job already use.
- Add a regression test that spies on Agent.countDocuments() and
asserts the active tenantStorage context is SYSTEM_TENANT_ID during
the call. Pins the wrap against future regressions without the
brittleness of toggling the strict-mode env var (which caches on
first read).
Note: the delete-time and save-time paths already run inside an
authenticated HTTP request where tenantStorage.run is set by auth
middleware, so the cleanup naturally scopes to the current tenant —
which is the correct behavior there since file ownership is
tenant-scoped.
* 🧹 chore: Drop Unused path Import From Process Integration Spec
Leftover from an earlier iteration that resolved the migration path
via path.resolve before I switched to a relative require. The import
does nothing now — removing it.
* fix: prevent browser autofill from silently dropping MCP customUserVars on save
The customUserVars form in CustomUserVarsSection renders each
credential as a plain `<input type="text">` with no autofill
guards. This caused two user-visible problems:
1. Browser password managers (Chrome's built-in, 1Password, LastPass,
Bitwarden) treat the fields as savable and offer to store values,
which undermines the per-user credential model -- the whole point
of customUserVars is that each user's own secrets stay in their
own session and backend-encrypted, not cached by the browser.
2. When autofill fills a field, it does so via DOM mutation that does
NOT fire React's synthetic `onChange`. react-hook-form's
Controller therefore never sees the value, the form state stays
`""`, and on submit the backend receives an empty string for
every affected field. The user's typed credentials are silently
dropped -- LibreChat stores 16 encrypted-empty-string rows in
pluginauths, tool calls subsequently fail with "API key not set"
errors, and the UI shows an "Unset" pill next to fields the user
is certain they filled in.
The fix matches the pattern already used in
`SidePanel/Builder/ActionsAuth.tsx` for the analogous actions
credential input:
type="new-password"
autoComplete="new-password"
plus vendor-specific ignore attributes for LastPass and 1Password:
data-lpignore="true"
data-1p-ignore="true"
(Modern browsers ignore `autocomplete="off"` for password-shaped
fields, so the vendor attributes are the reliable defence against
LastPass and 1Password, which have their own heuristics.)
No behavioural change beyond preventing autofill: the input still
accepts typed or pasted values, react-hook-form still collects them,
submit still writes to the backend. The difference is that the
values now actually reach the submit handler.
* test: add regression guard for MCP customUserVars autofill prevention
Condenses the rationale comment on the credential `<Input>` and adds a
render test asserting that the autofill-prevention attributes
(`type`, `autoComplete`, `data-lpignore`, `data-1p-ignore`) remain on
the rendered input. The underlying bug (browser DOM mutations
bypassing React's synthetic onChange) is severe enough that a future
refactor accidentally dropping these attributes would silently
re-introduce credential data loss.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
Registers text/calendar across the MIME allowlists (fullMimeTypesList,
codeInterpreterMimeTypesList, textMimeTypes regex) and maps the .ics,
.ical, .ifb, .icalendar extensions in codeTypeMapping, so iCalendar
files produced by the code interpreter are accepted as valid output
and rendered as downloadable attachments.
* 📦 chore: Bump @modelcontextprotocol/sdk to v1.29.0
* ♻️ refactor: Extract WWW-Authenticate Probe Helper for MCP OAuth
* 🔐 fix: Prefer WWW-Authenticate resource_metadata Hint for MCP OAuth
Per RFC 9728 §5.1, the `resource_metadata=<url>` parameter in a 401
`WWW-Authenticate: Bearer` challenge is the authoritative protected-resource
metadata source. Path-aware `.well-known` discovery was winning over the
hint, so split deployments that serve valid-but-wrong metadata at the
path-aware endpoint stranded OAuth at defunct authorization servers.
Threads the hint through `discoverOAuthProtectedResourceMetadata` via
`opts.resourceMetadataUrl` in both startup detection and the OAuth handler,
matching the behavior of Claude Desktop, the MCP Inspector, OpenAI tooling,
and Microsoft Copilot Studio.
Fixes#12761.
* 🧵 fix: Thread OAuth-Aware fetchFn Through Resource-Metadata Probe
Without this, admin-configured `oauthHeaders` (e.g. a gateway API key that
fronts the MCP endpoint) were stripped from the probe, causing the gateway
to 401 for the wrong reason and masking the real `WWW-Authenticate` hint.
The helper now accepts a FetchLike and defaults to global fetch, so the
startup detection path is unchanged while the handler passes its OAuth-
aware wrapper through.
* 🧹 refactor: Address MCP OAuth Probe Review Findings
- Thread `fetchFn` through `probeResourceMetadataHint` so admin-configured
`oauthHeaders` reach the probe (a gateway API key that fronts the MCP
endpoint would otherwise 401 us for the wrong reason and hide the real
Bearer challenge).
- Skip the redundant HEAD request in `checkAuthErrorFallback` when the
probe already observed a 401/403; fall back to a fresh HEAD only when
every probe attempt threw (transient network error).
- Narrow the oauth barrel: drop `export * from './resourceHint'` so the
helper stays an internal module.
- Add `scope` extraction coverage (`Bearer scope="read write"`) and a
403-only observation path; isolate `MCP_OAUTH_ON_AUTH_ERROR=true` in a
dedicated suite so precise-outcome tests aren't muddied by the safety net.
* ✅ fix: Use Zod Schema in MCP Reconnection-Storm Test Tool
MCP SDK 1.28 tightened `McpServer.tool()` to require Zod schemas instead
of plain JSON-Schema objects. Swap the `{ message: { type: 'string' } }`
shape for `z.string()` so the fixture server spins up under SDK 1.29.
* 🛡️ fix: Harden MCP OAuth resource_metadata Hint Against SSRF
The `resource_metadata` URL is echoed from an untrusted MCP server, so
handing it straight to the SDK lets a malicious server redirect discovery
at private IPs, the cloud metadata service, or any host the admin did not
intend to reach. Caught by the Copilot review on #12763.
- `handler.ts`: run the hint through the same `validateOAuthUrl` /
`allowedDomains` gate that already guards the authorization-server URL;
drop it and fall back to path-aware discovery on rejection.
- `detectOAuth.ts`: no admin-scoped allowedDomains here, so apply a
strict `isSSRFTarget` + DNS resolution check and silently discard any
hint pointing at a private/loopback/metadata address.
- Tests cover both the hostname-list and DNS-resolution rejection paths
and assert the SDK falls back to path-aware discovery unharmed.
* 🧪 test: Mock ~/auth in fallback Suite for Consistency
Matches the main `detectOAuth.test.ts` mock so the SSRF guards added in the
previous commit don't touch the real `~/auth` module at test time.
* 🔍 fix: Scope OAuth Fallback to HEAD + Parse Multi-Scheme WWW-Authenticate
Two codex findings on #12763:
- **P1**: the merged `authChallenge` flag was letting POST-only 401/403 flip
the `MCP_OAUTH_ON_AUTH_ERROR` fallback, misclassifying WAF/CSRF-hardened
endpoints (HEAD 200 + POST 403) as OAuth-required. Rename to
`headAuthChallenge` and derive it only from the HEAD probe, matching the
legacy fallback's HEAD-only semantics. Add a regression test.
- **P2**: the SDK's `extractWWWAuthenticateParams` only inspects the first
scheme token, so multi-scheme headers like
`Basic realm="api", Bearer resource_metadata="..."` silently dropped the
authoritative Bearer hint. Fall back to a regex across the full header
when the SDK returns nothing but Bearer is present. Add a regression
test covering the multi-scheme case.
* 🧽 refactor: Tighten MCP OAuth Probe Semantics
Addresses the second external review pass plus codex P2:
- Merge the two stacked JSDoc blocks on `probeResourceMetadataHint` into one
with a proper `@returns` section.
- Only short-circuit HEAD when it delivered the `resource_metadata` hint
itself — a Bearer-without-params HEAD now lets POST run, since some
servers surface their hint only on POST and we were missing it.
- Drop the unused `scope` field from `ResourceHintProbeResult`; no caller
read it, and YAGNI beats a reserved field.
- Remove the redundant `OAUTH_ON_AUTH_ERROR` guard inside
`checkAuthErrorFallback` — the only call site already gates on it.
- Codex P2: signal "HEAD status unknown" via `null` when the HEAD probe
threw and POST returned non-auth. Previously that combination leaked a
`{headAuthChallenge: false}` result and silently skipped the fallback's
retry HEAD, which could misclassify OAuth-required servers after a
transient HEAD failure.
Regression tests cover every path: Bearer-no-hint-on-HEAD + hint-on-POST,
multi-scheme `Basic + Bearer` headers, HEAD-threw + POST-200 retry, and
the WAF/CSRF-only POST 403 case.
* 🪥 polish: Tighten Probe Null-Guard Ordering + Add Malformed-Hint Test
Two NITs from the follow-up review:
- Move `bearerChallenge` computation after the `!wwwAuth` guard so the
variable is only derived when it can be meaningfully `true`. The
early-return path is now a clean unconditional exit.
- Add a regression test that asserts `Bearer resource_metadata="not-a-url"`
yields `resourceMetadataUrl: undefined` without throwing, locking in
the try/catch safety net in `extractHintFromHeader` and the SDK
parser alike.
* 🔒 fix: Validate MCP OAuth Protected Resource Metadata binding (GHSA-gvpj-vm2f-2m23)
RFC 9728 §3.3/§7.3 requires clients to verify that the `resource` identifier
advertised by an OAuth Protected Resource Metadata document matches the URL
used to fetch it. Without this check, a malicious MCP server can serve
metadata pointing at a legitimate server's `authorization_servers`, causing
LibreChat to obtain an access token for the real server and send it to the
attacker in subsequent API calls.
Validation happens at discovery time so the entire metadata document is
discarded on mismatch — `authorization_servers` on a spoofed document is
equally untrustworthy and is the primary theft vector in the PoC.
Uses the MCP SDK's own `checkResourceAllowed` (origin + path-prefix) for
semantic parity with `selectResourceURL`, a SDK code path LibreChat bypasses.
This is looser than RFC-strict equality (handles common cases like
`/mcp/sse` server vs `/mcp` advertised resource, or trailing-slash
normalization) while still rejecting cross-origin spoofs and same-origin
sibling-path confusion.
* 🛡️ fix: Re-validate Resource Binding at MCP OAuth Token Exchange
Adds a defense-in-depth re-assertion of the RFC 9728 §3.3 resource/server
binding inside `completeOAuthFlow`. Flows have a 10-minute TTL, so a flow
initiated under pre-fix (vulnerable) code could still be in-flight at upgrade
time carrying unvalidated resource metadata. Re-checking here closes that
window without requiring operators to flush flow state on deploy.
Also guards against future regressions that might re-introduce unvalidated
paths into the flow-metadata pipeline (GHSA-gvpj-vm2f-2m23).
* ✅ test: Address Review Findings on MCP OAuth Resource Validation
Follow-up to GHSA-gvpj-vm2f-2m23 fix. Resolves three reviewer findings:
- Assert `failFlow` is called when `completeOAuthFlow` rejects at
re-validation — locks in the "no stuck PENDING entry" guarantee that the
catch block already provides in production code.
- Update the "no resource metadata" warning in `initiateOAuthFlow`: post-fix,
that branch is only reachable when PRM discovery returned nothing (404,
network error, server without RFC 9728). A document with a missing
`resource` field now throws earlier in `assertResourceBoundToServer`, so
the old "missing 'resource' property" phrasing described a case that can
no longer reach this branch.
- Add a test for an unparseable `resource` string triggering the
error-wrapping path in `assertResourceBoundToServer` (verifies the wrapper
surfaces a descriptive message instead of leaking a raw `TypeError:
Invalid URL` from the SDK's `new URL()` call).
- Bumped the version of @librechat/agents from 3.1.67 to 3.1.68 in multiple package.json files to ensure consistency and access to the latest features and fixes.
- Updated package-lock.json to reflect the new version and maintain dependency integrity.
* 🔧 fix: resolve custom-endpoint providers for summarization
When `summarization.provider` in `librechat.yaml` is set to a custom-endpoint
name (e.g. `Ollama`), the string was passed verbatim to the agents SDK, which
only knows a fixed set of provider names and threw
`Unsupported LLM provider: Ollama`.
Before shaping the summarization config for the SDK, resolve the provider
through `getProviderConfig`: custom-endpoint labels are remapped to the
underlying SDK provider (e.g. `openAI`) and the endpoint's baseURL/apiKey are
injected into `parameters` so the summarization model reaches the right
backend, even when summarization targets a different custom endpoint than the
main agent.
Unknown names and names that appear with no matching endpoint flow through
unchanged so the SDK can surface a clear error. User-provided credentials and
unresolved env-var references are skipped rather than forwarded, letting the
SDK's self-summarize path reuse the agent's own clientOptions.
Ref: LibreChat Discussion #12614
* address: widen unresolved-env-var guard, fix test naming
- Reject summarization overrides when the extracted baseURL/apiKey still
contains any `${...}` placeholder, including prefix/suffix patterns like
`https://${UNSET}.example.com` that `envVarRegex` (exact-match) missed.
- Rename the "case-insensitive" test to reflect that only `Ollama` is
normalized via `normalizeEndpointName`; add coverage proving other
custom-endpoint names match case-sensitively.
* address: use req.config in responses.js; forward full endpoint options
- `responses.js` relied on a module-level `appConfig` set via `setAppConfig`,
which is never called anywhere. Use `req.config` directly so the
summarization provider resolver actually runs on the responses route.
- Route the custom endpoint config through `getOpenAIConfig` so summarization
inherits the same `headers`, `defaultQuery`, `addParams`/`dropParams`, and
`customParams` transforms (Anthropic/Google/etc.) that `initializeCustom`
applies for the main agent flow. Strip the stale `model`/`modelName`
defaults so `summarization.model` still wins.
* address: skip overrides when summarization matches agent endpoint
When `summarization.provider` resolves to the same custom endpoint as the
main agent, rely on the SDK's self-summarize path (which reuses
`agentContext.clientOptions` unchanged) rather than injecting overrides.
Otherwise the shallow spread of `clientOverrides.configuration` would
replace the agent's request-resolved state (dynamic headers, proxy/fetch
options) with yaml-only config.
Only applies when summarization targets a *different* endpoint from the
agent; the yaml config is all we have in that case, so overrides still
flow through.
* address: preserve raw provider when overrides cannot be built
When summarization points at a different custom endpoint than the agent
and we can't resolve the endpoint's credentials (user_provided, or a
still-unresolved `${VAR}` after env extraction), remapping to `openAI`
without overrides would silently route summaries to the default OpenAI
client. Preserve the raw provider name so the SDK raises a clear
"Unsupported LLM provider" error (now also logged, via the agents SDK
defense-in-depth fix) instead of sending traffic to the wrong backend.
* address: resolve endpoint headers and forward PROXY to summarization
- Custom-endpoint `headers` now flow through `resolveHeaders` before
reaching `getOpenAIConfig`, matching the main agent path. This ensures
templated values like `\${PORTKEY_API_KEY}` or `{{LIBRECHAT_BODY_...}}`
are substituted for summarization requests instead of being forwarded
literally.
- `PROXY` env var is now passed into `getOpenAIConfig` so cross-endpoint
summarization honors outbound proxy dispatchers configured for the rest
of the deployment.
* address: user summarization parameters win over endpoint defaults
Flip the merge order so `summarization.parameters` from yaml override
`clientOverrides` defaults (which come from `getOpenAIConfig` and always
include `streaming: true` etc.). A user who sets `parameters.streaming:
false` in their config should still see non-streaming summarization for
providers that require it.
* address: review feedback (logging, dead code, DRY, types, deep-merge)
- Log error in the resolveSummarizationProvider catch-all so programming
bugs in getProviderConfig/getOpenAIConfig/resolveHeaders surface in
operator logs instead of falling through silently.
- Drop dead `setAppConfig`/`appConfig` infrastructure in responses.js and
fix adjacent `allowedProviders` reference that also relied on the
never-initialized module-level appConfig. Uses `req.config` directly.
- Import canonical `normalizeEndpointName` from librechat-data-provider
instead of duplicating it locally.
- Replace `SummarizationClientOverrides = Record<string, unknown>` with
an explicit interface covering the known fields.
- Deep-merge `configuration` when user-supplied `summarization.parameters.
configuration` overlaps the resolved endpoint configuration, so user
additions (e.g. `defaultQuery`) don't wipe out `baseURL`/`defaultHeaders`.
- Wrap `process.env` mutations in test in `try/finally` so a failed
assertion doesn't leak env state into subsequent tests.
- Drop `as unknown as AppConfig` in test helper; fixture now matches the
`AppConfig` shape directly using a `Partial<TEndpoint>` union.
- Trim JSDoc that restated the name it was attached to.
* address: review nits — import order, local test type, conflict test
- Move `import { logger }` up into the package value-imports section so
it no longer sits between `import type` blocks.
- Replace `as unknown as SummarizationConfig['parameters']` in the
deep-merge test with a named `TestSummarizationParameters` type and a
single narrowing cast at the call site, making intent explicit.
- Add a test proving that user-supplied `configuration.baseURL` wins
over the resolved endpoint baseURL, locking in the deep-merge's
user-wins-on-conflict semantics that the previous suite only exercised
additively.
* 🤝 fix: load handoff sub-agents on OpenAI-compat endpoints (#12726)
Extracts the BFS discovery + ACL-gated initialization of handoff sub-agents
into a shared `discoverConnectedAgents` helper in `@librechat/api` and
wires it into the OpenAI-compatible `/v1/chat/completions` and Open
Responses `/v1/responses` controllers. These endpoints previously only
passed the primary agent config to `createRun` while keeping
`primaryConfig.edges` intact, which forced `MultiAgentGraph` into
multi-agent mode without loading the referenced sub-agents and caused
StateGraph to throw "Found edge ending at unknown node <id>".
The discovery helper also filters orphaned edges (deleted sub-agents or
those the caller lacks VIEW permission on), so API users see the same
graceful fallback the chat UI already had.
* 🧪 fix: use ServerRequest in discovery spec helpers
CI `tsc --noEmit -p packages/api/tsconfig.json` caught that the test
helpers typed `req` as `express.Request`, which is not assignable to
`DiscoverConnectedAgentsParams.req` (typed as `ServerRequest` whose
`user` is `IUser`). Local jest passed because ts-jest is transpile-only,
but the CI typecheck uses the full compiler.
* 🪲 fix: drop orphan edges on both endpoints, not just `to`
Addresses the P1 codex finding on #12740: `filterOrphanedEdges`
previously only removed edges whose `to` referenced a skipped agent.
Edges whose `from` was a skipped agent — the symmetric case in a
bidirectional graph like `A <-> B` where `B` is deleted or the user
lacks VIEW on it — leaked through to `createRun` and re-triggered
`Found edge ending at unknown node <id>` at StateGraph compile time.
The filter now drops an edge if either endpoint references a skipped
id, and the existing `to`-only test cases were updated to reflect the
stricter behavior. Adds a bidirectional-graph regression test in
`discovery.spec.ts`.
* 🔒 fix: enforce REMOTE_AGENT ACL on handoff sub-agents for API routes
Addresses the second P1 codex finding on #12740: the OpenAI-compat
`/v1/chat/completions` and Open Responses `/v1/responses` routes gate
the primary agent on `REMOTE_AGENT` (via `createCheckRemoteAgentAccess`),
but `discoverConnectedAgents` was checking handoff sub-agents against
the looser in-app `AGENT` resource type. That allowed a remote caller
who could reach the orchestrator but had only in-app visibility on a
sub-agent to invoke it via the API — bypassing the remote-sharing
boundary.
Adds an optional `resourceType` param to `discoverConnectedAgents`
(defaulting to `AGENT` for the chat UI path) and passes
`ResourceType.REMOTE_AGENT` from both API controllers so every
discovered sub-agent clears the same sharing boundary enforced at
route entry.
* 🧯 fix: enforce allowedProviders for discovered sub-agents
Addresses the third P1 codex finding on #12740: `discoverConnectedAgents`
forwarded the caller's `endpointOption` verbatim into `initializeAgent`,
but on the OpenAI-compat routes that option's `endpoint` is the primary
agent's provider (e.g. `openai`), not `agents`. `initializeAgent` only
enforces `allowedProviders` when `isAgentsEndpoint(endpointOption.endpoint)`
is true, so handoff sub-agents silently bypassed the provider allowlist
configured under `endpoints.agents.allowedProviders`.
Override `endpointOption.endpoint` to `EModelEndpoint.agents` for every
per-sub-agent init call. The primary agent still uses the caller's
endpointOption as before — this only affects the BFS-loaded handoff
targets. Regression test asserts the override.
* ✂️ fix: prune unreachable sub-agents after orphan-edge filtering
Addresses the fourth P1 codex finding on #12740: BFS eagerly initializes
every sub-agent referenced in the primary's edge scan, but once
`filterOrphanedEdges` drops edges whose endpoints were skipped, some of
those sub-agents end up disconnected from the primary. In an `A -> B ->
C` graph (edges stored directly on A) where B is skipped (missing or
no VIEW), both edges are filtered, but C was already loaded and would
still be passed to `createRun` — which flips into multi-agent mode on
`agents.length > 1` and turns C into an unintended parallel start node.
After filtering edges, compute the set of agent ids reachable from the
primary through the surviving edge set and prune `agentConfigs` to that
set. Two regression tests added: one for the pruning case, one that
confirms agents connected via surviving edges are still kept.
* 🔁 fix: don't seed initialize.js agentConfigs from the pre-pruning callback
Addresses the fifth P1 codex finding on #12740: `onAgentInitialized`
fires during BFS, BEFORE the helper prunes agents that become
disconnected once `filterOrphanedEdges` runs. Writing the sub-agent
straight into the outer `agentConfigs` there and then only additively
merging the pruned `discoveredConfigs` left stranded entries in the
outer map, and `AgentClient` would still hand them to `createRun` as
extra parallel start nodes (the exact failure mode the pass-4 prune
was meant to eliminate for the API controllers).
Drop the `agentConfigs.set` from the callback and replace the additive
merge with a direct copy from `discoveredConfigs`, which is now the
single authoritative source of what the run should see. The
per-agent tool context map is still populated during BFS — stale
entries there are harmless because they're only read by closure inside
`ON_TOOL_EXECUTE` and are unreachable once the agent is not in
`agentConfigs`.
* 🔬 fix: address audit findings on discovery helper
Resolves findings from a comprehensive external audit of #12740.
**Finding 1 (CRITICAL) — stale edges survive the reachability prune.**
The pass-4 prune removed unreachable agents from `agentConfigs` but left
matching edges in the return value. In an `A -> B -> C -> D` graph (all
edges stored on A) where B is skipped, `filterOrphanedEdges` drops A->B
and B->C but keeps C->D (neither endpoint is skipped). The caller then
sees `agentConfigs` without C/D but `edges` still references them,
flipping `createRun` into multi-agent mode with mismatched agents/edges
— the exact crash this PR is supposed to fix. Now filter the edge list
to the reachable set in the same pass, so the returned shape is
self-consistent: every edge endpoint is either the primary id or a key
of `agentConfigs`. New regression test covers A->B->C->D with B skipped.
**Finding 2 (MAJOR) — unconditional `getModelsConfig` on every API
request.** The OpenAI-compat and Responses controllers called
`getModelsConfig(req)` and `discoverConnectedAgents` even when the
primary agent had no edges (the common single-agent API case). Gate
both behind `primaryConfig.edges?.length > 0` so single-agent runs
don't pay that cost.
**Finding 5 (MINOR) — silent mutation of caller's
`primaryConfig.userMCPAuthMap`.** The helper aliased that object and
then `Object.assign`'d sub-agent entries into it, changing the caller's
config in-place. Shallow-clone up front so the returned merged map is
the only destination.
**Finding 7 (NIT) — dead `?? []` coalescing.**
`filterOrphanedEdges` always returns a concrete array, so the
`discoveredEdges ?? []` fallback was never reached. Simplified the
`primaryConfig.edges = …` assignment.
Also adds a test that verifies `primaryConfig.userMCPAuthMap` is not
mutated in-place.
* 🧹 chore: address audit NITs on discovery helper
Addresses two NIT findings from the post-fix audit:
**F1** — the shallow clone on `primaryConfig.userMCPAuthMap` was only
applied on the primary side; the `else` branch (hit when the primary
had no MCP auth and the first sub-agent seeds the map) assigned the
sub-agent's `config.userMCPAuthMap` directly, so a later sub-agent's
`Object.assign` mutated the first one's map in place. Harmless in
practice (per-request ephemeral objects) but asymmetric. Clone in the
else branch too. Test added.
**F2** — `initialize.js` had a defensive `if (agentConfigs.size > 0 &&
!edges) edges = []` normalizer. Pre-existing dead code: the helper now
always returns a concrete array from `filteredEdges.filter(...)`.
Removed for clarity.
* 🕸 fix: require all sources reachable when traversing fan-in edges
Addresses the seventh P1 codex finding on #12740: the reachability BFS
advanced through an edge as soon as any of its `from` endpoints matched
the current frontier node (`sources.includes(current)`), but the
subsequent edge filter required ALL sources to be reachable (`every`).
The two-semantics mismatch let a fan-in edge like `{from: ['A','B'],
to: 'C'}` mark C reachable purely via A even when B had no path from
the primary, then drop the edge itself at filter time. Result: C
survived in `agentConfigs` with no surviving edge connecting it to A,
so `createRun` flipped into multi-agent mode on `agents.length > 1`
and C ran as an unintended parallel root.
Replace the BFS with a fixed-point iteration keyed on the same
all-sources-reachable predicate used by the filter, so traversal and
filtering stay aligned and multi-source edges only fire once every
source is in the reachable set.
Two regression tests added:
- `{from: ['A','B'], to: 'C'}` with B having no incoming path — asserts
neither B nor C leak into the result.
- `A -> B`, `A -> C`, `['B','C'] -> D` — asserts the fan-in edge fires
and D becomes reachable once both B and C are.
* 🔀 fix: match SDK OR semantics for multi-source edge reachability
Reverts the all-sources-required reachability gate from 4982f1c3b and
replaces it with an any-source-reachable model, which matches how
`@librechat/agents`'s `MultiAgentGraph.createWorkflow` actually wires
multi-source edges at runtime (per-source `builder.addEdge(source,
destination)`). With the previous `every` gate, a legitimate handoff
edge `{ from: ['A', 'B'], to: 'C' }` where B had no incoming path was
pruned along with C, regressing OR-semantics routing that the SDK
would otherwise handle correctly.
New behavior:
1. Reachability: an edge advances when ANY of its `from` endpoints is
already reachable. Fixed-point iteration over `filteredEdges`.
2. Edge filter: keep an edge when it has at least one reachable source
AND all destinations are reachable (a missing destination would
still crash `StateGraph.compile` with `Found edge ending at unknown
node`).
3. Agent prune: keep agents that are reachable OR referenced on any
endpoint of a surviving edge. The second clause preserves co-sources
in multi-source edges (B in `{ from: ['A','B'], to: 'C' }` when
nothing else reaches B) so the SDK's per-source `addEdge` — and the
`validateEdgeAgents` safety-net I added to the SDK in #111 — still
finds B as a node.
The pass-audit A->B->C->D regression test continues to pass: with B
skipped, `filterOrphanedEdges` drops both B-adjacent edges, reachability
never expands past A, C->D has no reachable source so it gets filtered,
and C/D are pruned because they're neither reachable nor referenced.
* ✂️ fix: strip skipped co-members from multi-source/multi-dest edges
Addresses codex pass-9 P2 on #12740. `filterOrphanedEdges` previously
dropped an edge whenever any `from` id was skipped, which was correct
for scalar edges but over-aggressive for multi-source ones: the agents
SDK adds one `builder.addEdge(source, destination)` per source, so
`{ from: ['A','B'], to: 'C' }` with B skipped still has a valid
`A -> C` route that was being thrown away.
Now sanitize each endpoint:
- Scalar skipped → drop the whole edge (no route survives).
- Array with some skipped → strip the skipped ids, keep the edge with
the surviving members. If the array empties out, drop the edge.
Symmetric handling for `to` covers multi-destination fan-out when one
co-destination is skipped. Tests updated/added:
- `strips skipped co-sources from multi-source edges…`
- `strips skipped co-destinations from multi-destination edges`
- `drops multi-member edges only when every member on a side is skipped`
- Discovery-side: `preserves valid routes when one co-source of a
multi-source edge is skipped` asserts the end-to-end behavior —
skipped co-source B gets stripped from the edge, A->C routing
survives, and C remains in `agentConfigs`.
* 🔓 fix: respect SHARE-on-AGENT fallback for handoff ACL on API routes
Addresses codex pass-10 P1 on #12740. The API controllers were handing
`discoverConnectedAgents` a raw `PermissionService.checkPermission` call
against `ResourceType.REMOTE_AGENT`, but the route-level middleware
(`createCheckRemoteAgentAccess`) authorizes the primary agent via
`getRemoteAgentPermissions`, which first consults the AGENT ACL and
treats owners with the SHARE bit as remotely authorized even without
an explicit REMOTE_AGENT grant. The mismatch meant a user could open
the primary via `/v1/chat/completions` or `/v1/responses`, but their
own owned handoff sub-agents were silently skipped — breaking
multi-agent handoffs for the common "owner runs their own multi-agent
orchestrator" case.
Both controllers now pass `discoverConnectedAgents` a `checkPermission`
wrapper that delegates to `getRemoteAgentPermissions` (with
`getEffectivePermissions` injected from `PermissionService`) and
compares the returned bitmask against the required permission via
`hasPermissions`. Sub-agents are now authorized by the exact same
rules the route middleware applies to the primary.
* 🌱 fix: preserve user-defined parallel-start branches
Addresses codex pass-11 P2 on #12740. The post-filter reachability
prune seeded only from `primaryConfig.id`, which killed
`MultiAgentGraph`'s legitimate multi-start pattern — a user-defined
edge like `X -> Y` where X has no incoming path (X is an intentional
parallel starting node, run alongside the primary) was being dropped
because neither X nor Y was reachable from the primary.
Reconcile the tension with pass-4 ("prune accidental orphans when an
intermediate is skipped") by using pre-filter reachability as the
signal:
- An agent that WAS reachable from the primary via the original
(pre-filter) edges but loses that path when `filterOrphanedEdges`
runs is an accidental orphan (a skipped hop broke the chain) — prune.
- An agent that was NEVER reachable from the primary, even pre-filter,
is an intentional parallel start — seed it into post-filter
reachability so its component survives.
Surviving-edge endpoint references still keep an agent (co-sources in
multi-source edges). New test `preserves user-defined parallel-start
branches disconnected from the primary` covers the pass-11 scenario;
the existing `A->B->C->D, B skipped` regression test continues to
pass because C/D were pre-filter reachable through B and lose that
reachability after filtering.
* 🎯 fix: tighten parallel-start seed criterion to 'no pre-filter incoming edge'
Addresses codex pass-12 P1 on #12740. The pass-11 seed heuristic — 'agent
is in `agentConfigs` but was not pre-filter reachable from the primary' —
was too permissive. A downstream agent like Y in `X -> Y` where X gets
skipped (missing / no VIEW) was never pre-filter reachable from the
primary either, so the old rule promoted Y to a parallel start node and
discovery returned `agents: [primary, Y]` with no connecting edge. The
SDK then ran Y as an unintended parallel root — exactly the orphan
behavior pass-4 wanted to prevent.
Tighter criterion: seed a post-filter reachability root only when the
agent had NO incoming edge in the pre-filter graph. That matches
`MultiAgentGraph.analyzeGraph`'s "no-incoming-edge" definition of a
start node applied to the user's original declared topology, so:
- `A -> B` plus a user-defined `X -> Y` parallel branch: X has no
incoming pre-filter → seeded → X and Y both survive.
- `A -> B` plus `X -> Y` with X skipped: Y had an incoming pre-filter
(`X -> Y`) → NOT seeded → Y is pruned as the orphan it is.
- `A -> B -> C` with B skipped: C had an incoming pre-filter (`B -> C`)
→ NOT seeded → C is pruned.
New test `does not promote a downstream orphan to a parallel start when
its only upstream is skipped` locks in the pass-12 scenario. The pass-11
`preserves user-defined parallel-start branches` test continues to hold.
* 📁 fix: don't enforce AGENT-only file ACL on REMOTE_AGENT API callers
Addresses codex pass-13 P1 on #12740. When I refactored the API
controllers' DB-method bundle, I inadvertently started forwarding
`filterFilesByAgentAccess` into `initializeAgent`. That helper calls
`checkPermission` with `resourceType: ResourceType.AGENT`, but these
routes authorize callers through `REMOTE_AGENT` (via
`getRemoteAgentPermissions`). A user granted `REMOTE_AGENT_VIEWER` on
a shared agent but lacking direct `AGENT_VIEW` could invoke the agent
yet all its owner-attached context files would get silently filtered
out — breaking `file_search`/context retrieval for remote consumers.
Drop `filterFilesByAgentAccess` from the OpenAI-compat and Responses
controllers' `dbMethods` (and remove the now-unused import). The chat
UI's `initialize.js` keeps it since that path legitimately authorizes
at the AGENT level. No functional change inside the helper — passing
`undefined` simply tells `primeResources` to skip the per-file ACL
filter, restoring the pre-refactor API behavior.
* 🪓 fix: strip unreachable co-sources from surviving multi-source edges
Addresses codex pass-14 P1 on #12740. The earlier pass-8 fix kept any
agent referenced as an endpoint of a surviving edge (via a
`referencedByEdge` fallback) to avoid the SDK's `validateEdgeAgents`
failing on missing nodes. But that fallback propped up unreachable
co-sources too: with `[A -> C, X -> B, [B,C] -> D]` and X skipped,
`X -> B` gets filtered, the `[B,C] -> D` fan-in survives because C is
reachable, and B stays in `agentConfigs` solely because the fan-in
still lists it. `MultiAgentGraph.analyzeGraph` then sees B with no
incoming edge and runs it as an unintended parallel root.
Sanitize surviving edges instead: for a kept edge whose `from` is an
array, filter out any co-source that isn't reachable. The SDK's
per-source `addEdge` fires independently, so dropping an unreachable
co-source doesn't invalidate the remaining routes — in the scenario
above `[B,C] -> D` becomes `[C] -> D`, every endpoint of every
surviving edge is now reachable, and the agent prune collapses to a
strict `reachable.has(agentId)` check. No more referenced-by-edge
fallback.
Regression test added: `strips unreachable co-sources from surviving
multi-source edges (no stray parallel root)` — asserts B is absent
from every surviving edge endpoint and the fan-in's `from` is just
`['C']`. All 22 prior discovery tests still pass unchanged.
* 🔊 fix: Preserve Log Metadata on Console for Warn/Error Levels
The default console formatter discarded every structured metadata key on the
winston info object — only `CONSOLE_JSON=true` preserved it. That meant
failures emitted by the agents SDK (e.g. "Summarization LLM call failed")
reached stdout without the provider, model, or underlying error attached,
leaving users unable to diagnose the root cause.
- Add `formatConsoleMeta` helper to serialize non-reserved metadata as a
compact JSON trailer, with per-value string truncation and safe handling
of circular references.
- Append the metadata trailer to warn/error console lines; info/debug
behavior is unchanged.
- Relax `debugTraverse`'s debug-only gate so warn/error messages routed
through the debug formatter also surface their metadata.
- Add a `formatConsoleMeta` stub to the shared logger mock so existing
tests keep working.
* 🔐 fix: Also Redact Sensitive Patterns on Warn Console Lines
The warn-level console output now includes a metadata trailer that may
contain provider-returned error strings with embedded tokens or keys
(e.g. `Bearer ...`, `sk-...`). Apply `redactMessage` to warn lines in
addition to error, matching the new surface area.
* 🔐 fix: Redact Sensitive Tokens Embedded in JSON Metadata
Two gaps in the existing console redaction that became user-visible once
warn/error lines started emitting structured metadata:
1. The OpenAI-key regex (`/^(sk-)[^\s]+/`) was anchored to start-of-line,
so keys embedded inside JSON payloads (e.g. `{"apiKey":"sk-..."}`)
were never redacted. Every console line begins with a timestamp, so
the anchor effectively made this pattern dead code.
2. `formatConsoleMeta` stringified metadata values verbatim; a sensitive
string value was only redacted by the whole-line regex pass, which
missed the anchored `sk-` case above.
Fix:
- Drop the `^` anchor; add `/g` so every occurrence is redacted, not just
the first.
- Also exclude `"` and `'` from the token body so JSON-embedded values
terminate at the closing quote rather than chewing into the next field.
- Simplify `redactMessage` to apply patterns directly (dropping the
`getMatchingSensitivePatterns` filter) — the filter used `.test()`
which has stateful behavior on `/g` regexes and is no longer needed.
- `formatConsoleMeta` now runs `redactMessage` over every string value
before JSON serialization, so the metadata trailer is safe even on the
warn path.
- Add regression tests covering both fixes.
Reviewed-by: Codex (P1 finding on PR #12737, commit 68c31b6).
* 🔐 fix: Redact Metadata in debugTraverse for Warn and Error
Relaxing the debug-only gate in debugTraverse (in commit 59371be0)
routed warn/error records through the traversal path, which emits leaf
string values verbatim (via truncateLongStrings only). Because
DEBUG_LOGGING defaults to true, those records are also written to the
rotating debug log file — which means payloads like
`{ auth: 'Bearer ...' }` or `{ openaiKey: 'sk-...' }` were persisted
unredacted once my earlier change took effect.
Apply redactMessage to the final formatted string when the level is
warn or error. Debug-level behavior is unchanged (matching prior art).
Includes regression tests covering error/warn redaction and
debug-level preservation.
Reviewed-by: Codex (P1 finding on PR #12737, commit e288f7fd).
* 🔐 fix: Anchor Secret Regexes at Word Boundaries to Prevent Over-Redaction
Removing the `^` anchor in commit e288f7fd let the OpenAI-key regex match
anywhere in the line — including inside ordinary words like `task-runner`
or `mask-value`, where `sk-` appears mid-word. Non-secret text was being
rewritten to `task-[REDACTED]`, hiding real log content from operators.
- Anchor every sensitive-key pattern with `\b` so matches only fire at
word boundaries.
- Constrain the OpenAI-key body to the documented charset
(`[a-zA-Z0-9_-]+`) instead of the broader "not whitespace or quote"
character class.
- Add `&` to the `key=` exclusion so a query-string value stops at the
next parameter separator.
- Regression tests covering both the over-redaction cases (`task-runner`,
`monkey=10`) and the intended redactions still firing.
Reviewed-by: Codex (P2 finding on PR #12737, commit c09d293d).
* 🔐 fix: Redact Before Colorize To Survive ANSI Word-Boundary Interference
The console pipeline runs `redactFormat → colorize({ all: true }) → printf`.
With `all: true`, winston wraps `info.message` in ANSI escapes whose
trailing `m` is a word character. That means `\b(Bearer )…` placed at the
start of a colorized segment can fall on a (word,word) boundary and miss —
the earlier line-wise `redactMessage(line)` pass in printf suffers the
same issue because it runs after colorize.
Extend `redactFormat` to run for `warn` in addition to `error`, operating
on the raw pre-colorize `info.message` + `Symbol.for('message')` strings.
The later in-printf `redactMessage(line)` stays as a backstop, but the
primary redaction now happens where the regex can actually see the text.
Metadata redaction already operates on the raw info object via
`formatConsoleMeta`, so it was never affected by ANSI — no change there.
Includes regression tests for the new warn-level behavior and for the
info/debug no-op path.
Reviewed-by: Codex (P2 finding on PR #12737, commit fdb6b361).
* 🧹 fix: Prefer Structured Metadata Over Consumed Splat Args in Traversal
`debugTraverse` previously read `metadata[Symbol.for('splat')][0]` first
and only fell back to the structured metadata object. When a caller uses
printf interpolation alongside a metadata object — for example
`logger.warn('failed for %s', tenant, { provider })` — winston leaves the
*consumed* positional arg (`tenant`) in `SPLAT[0]` after interpolation.
The formatter would then append the tenant a second time and skip the
real metadata, regressing debug-file and `DEBUG_CONSOLE` output quality
now that warn/error share this path.
Prefer the structured metadata object (via `extractMetaObject`) and only
fall back to `SPLAT[0]` when there's nothing else, so the surviving log
line surfaces the actual key/value pairs regardless of call shape.
Reviewed-by: Codex (P2 finding on PR #12737, commit 1e43d636).
* 🧹 fix: Skip Consumed Splat Primitives in Warn/Error Debug Traversal
When no structured metadata is attached, winston still leaves consumed
`%s` / `%d` arguments in `Symbol.for('splat')`. Previous fix preferred
the structured object but still fell back to whatever sat at `SPLAT[0]`
— so `logger.warn('failed for %s', tenantId)` emitted
`failed for tenant-7 tenant-7` in the traversal path (debug file and
`DEBUG_CONSOLE`), now regressed outside of `debug` level because
warn/error share the path.
Only accept the splat fallback when the value is a plain object or an
array (structural data worth surfacing). Primitives there are almost
certainly consumed printf args and get skipped.
Regression tests cover the single-%s case and the array-as-metadata case
(which still surfaces through splat).
Reviewed-by: Codex (P2 finding on PR #12737, commit bccbf117).
* 🧹 fix: Skip Numeric Splat Keys When Extracting Log Metadata
When a caller passes a primitive as the second argument — e.g.
`logger.warn('Unhandled step creation type:', step.type)` — winston /
`format.splat()` can leave character-index keys (`"0"`, `"1"`, …) on the
`info` object. With the warn/error metadata trailer in play, those
synthetic artifacts were being surfaced as bogus metadata, producing
noisy console and debug-file output.
Filter out numeric-string keys in `extractMetaObject` so only real
metadata fields reach the trailer. Added a regression test.
Reviewed-by: Codex (P2 finding on PR #12737, commit b34628de).
* 🧹 fix: Preserve Unconsumed Primitive Splat Args in Debug Traversal
The previous round dropped every primitive SPLAT[0] value to avoid
duplicating consumed %s args, but that removed useful context from
calls like \`logger.debug('prefix:', detail)\` where the primitive was
never interpolated — users lost the \`detail\` value.
Refine the heuristic: skip a primitive splat value only when it already
appears inside the (post-interpolation) \`info.message\`; otherwise
surface it. Arrays and objects continue to surface unconditionally.
Regression test covers the 'prefix:', detail case.
Reviewed-by: Codex (P2 finding on PR #12737, commit 6bf9548f).
* 🧹 fix: Traverse Filtered Metadata, Not Raw Metadata, In debugTraverse
`debugTraverse` computes `extracted = extractMetaObject(metadata)` to
strip reserved keys, underscore-prefixed internals, and numeric splat
artifacts — but the later \`klona(metadata)\` + \`traverse\` path still
read the raw object, putting all the filtered junk back into the
rendered multi-line output.
Clone and traverse \`debugValue\` (the already-filtered object) instead.
Regression test exercises the case where numeric splat artifacts sit
alongside a real metadata field.
Reviewed-by: Codex (P2 finding on PR #12737, commit c29c18e8).
* 🧹 refactor: Split Warn/Error From Debug-Level Traversal in Parsers
Retrofitting `debugTraverse`'s multi-line object walker to cover
warn/error created a minefield of splat-interaction edge cases
(numeric artifacts, consumed %s args, bogus `_`-prefix filtering,
over-eager suppression of unconsumed primitives). Each fix kept
introducing new corner cases.
Split the two concerns instead:
- Warn/error now emit a compact single-line JSON metadata trailer via
`formatConsoleMeta`, then pass the full line through `redactMessage`.
This mirrors what the console formatter already does, so behavior
between the console and debug-file outputs stays consistent for
warn/error — and none of the splat/traversal edge cases apply.
- Debug level keeps its original code path verbatim (including the
raw `metadata` traversal and SPLAT\[0\] fallback). No regressions from
my earlier iterations.
- `extractMetaObject` no longer filters underscore-prefixed keys, so
legitimate fields like MongoDB `_id` still appear. Reserved winston
keys and numeric splat artifacts remain filtered.
Updated tests reflect the simpler contract (underscore preservation,
single-line trailer expectations already covered).
Reviewed-by: Codex (two P2 findings on PR #12737, commit 9ea11529:
`_id` regression and over-eager primitive suppression).
* 🧹 fix: Preserve Scalar Metadata When One Value Is Circular
`formatConsoleMeta` previously wrapped a single `JSON.stringify` in
try/catch — any circular reference inside any field (e.g. an attached
request/response object) caused the entire trailer to be dropped. That
defeats the goal of making failures diagnosable: one malformed field
would mask the provider/model/status we wanted to surface.
Use a `WeakSet`-based replacer that emits `[Circular]` for repeated
object visits. On the whole-object serialization failing, fall back to
per-field serialization so scalar keys always land and only the
offending field is replaced with \`"[Unserializable]"\`.
Reviewed-by: Codex (P2 finding on PR #12737, commit d63742a5).
* 🧹 fix: Address Audit Findings (JSDoc, Case-Insensitive Api-Key, Tests)
Audit review identified several MINOR/NIT items on top of the codex
rounds. This commit closes the actionable ones:
- **JSDoc (#1, #2)**: `extractMetaObject` no longer claims to filter
underscore-prefixed keys (that filter was removed intentionally for
MongoDB `_id`). `debugTraverse`'s docblock now describes the three
code paths (warn/error compact trailer, debug multi-line traversal,
other levels).
- **Case-insensitive api-key regex (#6)**: `/gi` so the Azure style
`Api-Key:` / `API-KEY:` also gets redacted. Pre-existing behavior
was lowercase-only.
- **Consolidated redundant branch (#5)**: `consoleFormat` printf was
checking `isError || isWarn` twice; merged into one block.
- **Pre-compiled regex (#9)**: `NUMERIC_KEY_RE` moved to module scope.
- **Test coverage (#3, #4)**: Added regression tests for
- per-field serialization fallback when a value's `toJSON` throws,
- sensitive strings nested inside metadata objects,
- the Azure-style `Api-Key:` header.
* 🐛 fix: accept documented `summarization.trigger.type` values
The Zod schema for `summarization.trigger.type` only accepted
`'token_count'`, but:
- the documentation lists `token_ratio`, `remaining_tokens`, and
`messages_to_refine` as valid
- the `@librechat/agents` runtime only evaluates those three types and
silently no-ops on anything else
The result was a double failure: any user following the docs hit a
startup Zod error, and anyone who matched the schema by using
`token_count` got a silent no-op at runtime where summarization never
fired.
Align the schema with the documented, runtime-supported trigger types.
Closes#12721
* 🧹 fix: bound `token_ratio` trigger value to (0, 1]
Per Codex review: the previous schema accepted `value: z.number().positive()`
for every trigger type. That meant `trigger: { type: 'token_ratio', value: 80 }`
(presumably meant as "80%") passed validation and then silently never fired —
because `usedRatio = 1 - remaining/max` is bounded at 1, so `>= 80` is always
false. That is exactly the silent-no-op pattern this PR is trying to eliminate.
Switch to a discriminated union so each trigger type has its own value
constraint:
- `token_ratio`: `(0, 1]` — documented as a fraction, so 80 is nonsense
- `remaining_tokens`: positive — token counts can be large
- `messages_to_refine`: positive — message counts can be > 1
Added tests for the upper-bound rejection and the inclusive upper bound
(`value: 1` still accepted as a valid "fire at 100%" extreme).
* 🧹 fix: accept `token_ratio: 0` per documented 0.0–1.0 inclusive range
Per Codex review: `.positive()` rejected `value: 0`, but the docs
describe the `token_ratio` range as `0.0–1.0` (both inclusive). Admins
who copy the documented lower bound into their YAML would fail schema
validation at startup.
Switch `token_ratio` to `.min(0).max(1)`. `0` is a valid (if extreme)
setting — the agents SDK's `usedRatio >= 0` check will fire as soon as
there is anything to refine, which is a legitimate "always summarize
when pruning happens" configuration.
`remaining_tokens` and `messages_to_refine` keep `.positive()`: both
are counts, and `0` there produces no meaningful behavior (the SDK
has an early return for `messagesToRefineCount <= 0`).
* 🐛 fix: preserve `token_ratio` trigger when `value: 0`
Per Codex review: now that the schema accepts `token_ratio: 0`,
`shapeSummarizationConfig` would silently drop it because of a truthy
check on `config?.trigger?.value`. The trigger would disappear and the
runtime would fall back to "no trigger configured" — which fires on any
pruning rather than honoring the explicit ratio.
Switch to `typeof value === 'number'`, which preserves `0` while still
rejecting `undefined`/`null`.
Added a regression test that asserts `{ type: 'token_ratio', value: 0 }`
survives the shaping function untouched.
* 🧹 fix: reject non-finite trigger values at schema level
Per Codex review: `z.number().positive()` still accepts `Infinity` and
`NaN` (via YAML `.inf`, `.nan`). Config validation would succeed, but
the agents SDK guards every trigger path with `Number.isFinite(...)` and
silently returns `false` — summarization never fires while the server
starts cleanly. That is the exact schema/runtime split this PR is trying
to eliminate.
Add `.finite()` to every trigger value. `token_ratio` already had an
implicit guard via `.max(1)`, but applying `.finite()` uniformly keeps
the intent obvious and catches `NaN` (which `.max(1)` does not).
* 🧹 fix: integer counts + targeted token_count migration warning
Two findings from the comprehensive review:
1. `remaining_tokens` and `messages_to_refine` are token/message counts
and are always integers in the runtime (`Number.isFinite(...)` guards
already assume integer semantics). `z.number().positive()` accepted
fractional values like `2.5`, which was semantically confusing and
would round oddly against the runtime's `>=` / `<=` comparisons. Add
`.int()` to both count-based branches; `token_ratio` stays fractional.
2. Anyone upgrading with `trigger.type: 'token_count'` in their YAML got
the generic "Invalid summarization config" warning plus a flattened
Zod error. Detect that specific case in `loadSummarizationConfig` and
emit a migration-friendly message that names the three valid
replacements. Export the function so the behavior is unit-testable.
Also added a parameterized passthrough test covering `remaining_tokens`
and `messages_to_refine` shaping, complementing the existing
`token_ratio` coverage.
* 🧹 fix: accurate fallback wording + bare-string trigger test
Two nits from the follow-up audit:
1. The legacy-`token_count` warning claimed "Summarization will be
disabled," but `shapeSummarizationConfig` treats a missing
summarization config as self-summarize mode (fires on every pruning
event using the agent's own provider/model). "Disabled" would
mislead an admin into stopping investigation. Reword to describe the
actual fallback and assert the new wording in the spec.
2. Add a regression test for the `trigger: 'bare-string'` YAML case, so
the `typeof raw.trigger === 'object'` guard is exercised rather than
implied.
3. Swap the en-dash in `(0–1)` for an ASCII hyphen so the log message
is safe in every terminal/aggregator regardless of UTF-8 handling.
* 🔇 fix: cast `raw.trigger.type` to inspect legacy value past narrowed union
CI TS check failed: after the schema tightening, `raw.trigger.type` is
narrowed to `"token_ratio" | "remaining_tokens" | "messages_to_refine"
| undefined`, so the runtime comparison to `"token_count"` is a
TS2367 ("no overlap") error even though that's exactly the comparison
we want for the migration guard.
Widen just that one access via `as { type?: unknown }` so the migration
check reads runtime-shaped YAML input without the type system folding
it back into the narrowed union.
* 🐛 fix: Preserve Raw Markdown on `Upload as Text`
When `RAG_API_URL` is configured, `.md` uploads were sent to the RAG API
`/text` endpoint, which routes Markdown through `UnstructuredMarkdownLoader`
and strips formatting (`#`, `**`, lists, blockquotes). Users expect `Upload
as Text` to preserve raw content - identical bytes in a `.txt` file round-trip
verbatim, while the `.md` came back stripped.
Short-circuit the RAG API call for Markdown files (by MIME type or `.md` /
`.markdown` extension) and read the file verbatim via `parseTextNative`.
Non-Markdown paths are unaffected, and the embedding path (`/embed`) keeps
its existing loader so vector search quality is unchanged.
* 🐛 fix: normalize markdown MIME and accept `text/md`
Addressing review feedback on the `Upload as Text` short-circuit:
- Accept `text/md` in the markdown MIME set (LibreChat treats it as a
valid markdown type elsewhere, e.g. the artifact-rendering prompt).
- Normalize the incoming MIME type (lowercase + strip parameters) before
the set lookup so parameterized values like
`text/markdown; charset=utf-8` and uppercase `TEXT/MARKDOWN` still
short-circuit. Extensionless uploads relying only on the `Content-Type`
header would otherwise fall through to the RAG `/text` endpoint and
lose their markdown formatting.
Extend `text.spec.ts` parametrized cases with `text/md`, parameterized
MIME, uppercase, and whitespace-padded variants.
* 🧹 chore: Address Code Review Follow-ups on `Upload as Text` fix
Addressing comprehensive review feedback:
- Debug log now includes filename and MIME type so operators can
identify which upload triggered the short-circuit without having
to correlate other logs.
- Expand markdown extension detection beyond `.md` / `.markdown` to
cover `.mdown`, `.mkdn`, `.mkd`, `.mdwn` (case-insensitive regex).
- Tighten `normalizeMimeType` parameter type from `string | undefined`
to `string` to match the actual Express.Multer.File type. The
falsy-check still protects against empty strings at runtime.
- Extend parametrized tests with the most common real-world shapes:
`text/plain` + `.md` (the MIME most browsers/servers assign),
the new rare extensions, and empty MIME + `.md` (pure extension
fallback path).
- Add a positive assertion that `readFileAsString` was called with the
expected arguments on every short-circuit case, so tests fail loudly
if the native-parse path ever regresses.
* 🧪 test: Cover `.mdwn` regex branch in Markdown short-circuit
Every other alternation in `MARKDOWN_EXTENSIONS_RE` has at least one
test case (`md`, `markdown`, `mdown`, `mkdn`, `mkd`) but `mdwn` did
not, leaving a typo in that branch undetectable.
* 🐛 fix: replace `$bitsAllSet` ACL queries for Cosmos DB compatibility
Azure Cosmos DB for MongoDB API does not implement the `$bitsAllSet`
query operator, so every permission check against Cosmos DB threw. The
five read paths in `aclEntry.ts` (`hasPermission`, `findAccessibleResources`,
`findPublicResourceIds`, and two sites in `getSoleOwnedResourceIds`) now
fetch candidate entries and apply the bitwise mask in application code.
This matches the existing FerretDB-compatible pattern.
Fixes#12729.
* 🐛 fix: delegate `findPubliclyAccessibleResources` to fixed DB method
`AccessControlService.findPubliclyAccessibleResources` inlined the same
`$bitsAllSet` query as the data-schemas layer, which fails on Azure
Cosmos DB for MongoDB. Delegate to `_dbMethods.findPublicResourceIds`
so a single implementation carries the Cosmos-compatible bitwise logic.
Refs #12729.
* 🐛 fix: move `$bitsAllSet` filter out of remote-agent aggregation
`enrichRemoteAgentPrincipals` used `$bitsAllSet` inside an aggregation
`$match`, which Azure Cosmos DB for MongoDB does not implement. Project
`permBits` through the pipeline and filter for `PermissionBits.SHARE` in
application code. The extra documents fetched are bounded by ACL entries
on a single agent resource, so the cost is negligible.
Refs #12729.
* 🧪 test: rename misleading public-dedup test and add real dedup coverage
The test previously named "returns deduplicated IDs even if the public
principal has multiple entries" only set up a single ACL entry, so it
did not actually exercise deduplication. Split into two tests: one for
the happy path (single entry with required bits), and one that bypasses
`grantPermission`'s upsert via `AclEntry.create` to confirm the
application-layer dedup Map handles genuine duplicates.
Refs #12729.
* 🧪 test: cover SHARE-bit filter in `enrichRemoteAgentPrincipals`
The `$bitsAllSet` match stage in `enrichRemoteAgentPrincipals` previously
guaranteed every aggregation row had SHARE; the Cosmos DB fix moved that
check into a JS `continue` branch with no direct coverage. Add a
dependency-injected unit test that stubs the aggregation with mixed
SHARE / non-SHARE / zero-bit rows and asserts only SHARE holders are
enriched and queued for backfill. Also includes a regression guard that
the `$match` pipeline stage no longer contains a `permBits` filter.
Refs #12729.
* ♻️ refactor: extract `filterByBitsAndDedup` helper for ACL reads
`findAccessibleResources` and `findPublicResourceIds` each inlined the
same bitmask-filter + `Map`-based dedup loop. Lift it into a private
`filterByBitsAndDedup(entries, requiredBits)` helper so the Cosmos-DB
compatible pattern lives in one place. Pure rename/extract — no
behavior change.
Refs #12729.
* 📝 docs: fix stale `\$bitsAllSet` references in FerretDB spec
The describe block and header comment in the FerretDB parity spec still
referenced `\$bitsAllSet queries` after the Cosmos DB compatibility fix
moved the bit mask into application code. Update the title to
\"Bitwise permission queries\" and rewrite the header comment to
describe the application-layer behavior being validated.
Refs #12729.
* ⚡ perf: push permission-mask filter back to the database via `$in`
The original fix for #12729 moved `$bitsAllSet` filtering into application
code, which meant every ACL read fetched the full set of rows for a
principal/resource and filtered in JS. For tenants with large ACL
collections this inflates wire transfer and heap.
Replace the JS filter with `permBits: { $in: permissionBitSupersets(X) }`.
For the 4-bit `PermissionBits` enum the `$in` list is at most 16 values
(8 for a single-bit mask like SHARE). `$in` is indexable and supported by
Azure Cosmos DB for MongoDB, so the filter runs on the server again —
restoring `.distinct('resourceId')` and `findOne()` semantics.
`permissionBitSupersets(requiredBits)` is memoized and exported from
`@librechat/data-schemas`. Callers restored:
- `hasPermission`: back to `findOne` short-circuit
- `findAccessibleResources` / `findPublicResourceIds`: back to `.distinct()`
- `getSoleOwnedResourceIds`: back to the `$match` + `$group` aggregation
- `enrichRemoteAgentPrincipals`: bit filter back in `$match`, JS `continue` removed
Refs #12729.
* 🧪 test: add `\$bitsAllSet` vs `\$in` parity + perf spec
Introduces `aclEntry.parity.spec.ts` — a side-by-side spec that runs the
legacy `\$bitsAllSet` query and the current `\$in`-based query against the
same `mongodb-memory-server` fixture and asserts identical output sets for
every affected method (`hasPermission`, `findAccessibleResources`,
`findPublicResourceIds`, `getSoleOwnedResourceIds`) across all 7 meaningful
permBits combinations.
Also logs median wall-clock time for the two query paths over 20 runs on
an 800-entry fixture, with a loose 3x guard against catastrophic
regressions. Initial local numbers: 1.05 ms vs 1.07 ms
(findAccessibleResources), 1.10 ms vs 1.05 ms (findPublicResourceIds).
Refs #12729.
* 🔒 hardening: freeze `permissionBitSupersets` cache + enum-shape guard
Two defensive changes from the comprehensive audit:
* Cached superset arrays are now `Object.freeze`d and the return type is
`readonly number[]`. Previously the cached arrays were returned by
mutable reference, so a caller that mutated the result would silently
corrupt the process-wide cache for every subsequent permission check.
`Object.freeze` turns that into a loud `TypeError` at the mutation
site. All existing call sites pass the result directly to Mongoose's
`$in`, which does not mutate.
* Added a module-load guard `if (MAX_PERM_BITS === 0) throw`. If
`PermissionBits` is ever refactored to a `const` object or string
enum, `Object.values(...).filter(isNumber)` would return `[]` and
`MAX_PERM_BITS` would silently become 0, making every query match no
rows and breaking every permission check. The guard fails loudly
instead.
Also collapsed four identical JSDoc lines across `hasPermission`,
`findAccessibleResources`, `findPublicResourceIds`, and
`getSoleOwnedResourceIds` into a single `{@link permissionBitSupersets}`
reference.
Refs #12729.
* 🧪 test: add focused unit tests for `permissionBitSupersets`
The helper is the single point of correctness for every ACL read path
(every query uses `permBits: { $in: permissionBitSupersets(X) }`), so it
warrants direct coverage independent of the higher-level parity and
behavior specs. Six cases added:
* `requiredBits=0` returns all 16 values
* `requiredBits=15` returns `[15]` only
* every returned value is a bitwise superset of `requiredBits`
* full parity against the `$bitsAllSet` definition for every
`required` in 0..15
* memoization: repeat calls return the same frozen reference
* frozen result throws `TypeError` on mutation attempts
Refs #12729.
* 🧪 test: tighten parity perf guard and document fixture constants
The `expect(currentMs).toBeLessThan(legacyMs * 3 + 50)` form was
dominated by the `+ 50` additive term at typical sub-ms query
latencies — at legacy=1ms, a 50x regression would still pass. Replace
with `Math.max(legacyMs * 5, 50)` so the multiplicative ceiling is
intact once the new path climbs out of the fixed noise floor.
Also added inline rationale for the `FIXTURE_SIZE = 800` and
`PERF_ITERATIONS = 20` constants.
Refs #12729.
* 🧹 chore: remove stale perf-guard comment, hoist rationale to describe
Commit a75f122 left a "Sanity check: A 3x multiplier" comment block
above the first perf guard, and 21852ac layered a second
"Multiplicative-dominant guard" block directly below it. The stale
block described the `legacyMs * 3 + 50` formula that no longer exists,
so both blocks coexisted and contradicted each other.
Delete the stale block from the first test, remove the redundant copy
from the second test, and lift the (now-single) rationale to the
enclosing `describe('performance')` block. Each test is now one
`expect` line — the rationale lives once, at the scope it applies to.
Refs #12729.
* 📝 docs: sharpen MAX_PERM_BITS guard message + test-sort consistency
Two NITs from the follow-up audit:
* The `MAX_PERM_BITS === 0` guard now names the specific refactors that
could cause it (const object / string enum / all-string shape) and
gives two concrete remediation paths (rewrite the reducer, or give
`permissionBitSupersets` an explicit bit-width parameter). Previously
the message just said "update permissionBitSupersets before
continuing", which was vague.
* The `requiredBits=15` unit test now applies the same
`[...result].sort((a, b) => a - b)` normalization as the other tests
so the set-equality assertions are uniform. The function happens to
return values in ascending order, but the helper's JSDoc does not
promise ordering, so sorting before comparing is the correct
defensive pattern.
Refs #12729.
* 🔒 fix: enforce `permBits <= MAX_PERM_BITS` at the schema level
Codex flagged a real silent regression: `permissionBitSupersets`
enumerates `$in` candidates only in `[0, MAX_PERM_BITS]` (currently 15),
so any ACL row with bits above the enum — e.g. `permBits = 31` from a
future role or a manual DB write — would be excluded from reads that
`$bitsAllSet` would have matched. Current write paths only produce
values in `[0, 15]` via the `PermissionBits` / `RoleBits` enums, so
no live data is affected, but the schema did not prevent out-of-range
writes, so the divergence was reachable.
Fix: add `min: 0`, `max: MAX_PERM_BITS`, and an `isInteger` validator to
the `permBits` field in the AclEntry schema. `MAX_PERM_BITS` is derived
from `PermissionBits` the same way `permissionBitSupersets` computes it,
so when a new bit is added to the enum both the read-side enumeration
and the write-side bound expand together.
Tests: four new cases cover the upper bound, over-limit, negative, and
non-integer inputs, each asserting `Mongoose.Error.ValidationError`.
Plus updated JSDoc on `permissionBitSupersets` to document the
invariant that the schema now enforces.
Refs #12729.
* ♻️ refactor: hoist `MAX_PERM_BITS` + enum-shape/ceiling guards to shared util
Addresses one MINOR DRY finding and one NIT defensive-guard finding from
the latest audit. `MAX_PERM_BITS` was duplicated in `methods/aclEntry.ts`
and `schema/aclEntry.ts` with identical computations; they could silently
diverge. Move the constant plus both module-load guards to
`src/common/permissions.ts`:
* `MAX_PERM_BITS === 0` guard — fail loudly if `PermissionBits` is ever
refactored to a `const` object or string enum (applied in both sites
now, not just methods).
* `MAX_PERM_BITS > 255` guard — circuit-breaker for the `$in` enumeration
strategy. At 4 bits the list tops out at 16; at 8 bits 256. Beyond
that the approach degrades, so fail at module load rather than emit an
unusably-large `$in` list.
Both the schema and the methods file now import from the single
`src/common/permissions.ts`. `readonly number[]` return type and
`Object.freeze` cache-value protection from the prior commits are
preserved.
Refs #12729.
* 🔒 fix: reject out-of-range `requiredBits` without caching (DoS fix)
Codex flagged a real unbounded-cache DoS vector:
`api/server/controllers/agents/v1.js` parses `req.query.requiredPermission`
via `parseInt` and forwards it unvalidated to `findAccessibleResources`,
which eventually calls `permissionBitSupersets(requiredBits)`. Because
the old code memoized *every* distinct input in a process-global Map,
an attacker could flood the cache with unique integers and grow memory
without bound.
Fix: when `requiredBits` is not an integer, is negative, or has any bits
set above `MAX_PERM_BITS`, return a single shared frozen empty array and
do NOT touch the cache. An empty `$in` list correctly matches zero rows
(rows cannot satisfy a bit we do not support), so the response is also
semantically correct — previously it "worked" only because invalid
inputs coincidentally expanded to `[]` too, but at the cost of a cache
entry each time.
Five new tests exercise the rejection path: upper-bound overflow, mixed
in-range+out-of-range bits, shared-instance identity across rejected
inputs, a 2000-unique-value cache-growth probe, and a regression check
that legitimate in-range inputs still get memoized normally.
Refs #12729.
* 🧪 test: probe `Map.prototype.set` to prove cache doesn't grow on rejection
Strengthens the DoS cache-growth test the review pass flagged: reference
identity alone allows a hypothetical regression like
`supersetCache.set(requiredBits, EMPTY_SUPERSETS); return EMPTY_SUPERSETS;`
to pass while still leaking one entry per attacker request.
Add a second test that spies on `Map.prototype.set`, records the global
call count before and after a burst of 1500 rejected inputs (500 each of
over-MAX integers, negatives, and non-integers), and asserts the delta
is zero. Manually verified the test has teeth: injecting the
hypothetical regression in the implementation produced exactly 1500
extra `Map.set` calls and the new test failed as expected; reverting
restored the clean state.
Renames the existing identity-based test to `(reference identity)` so
its scope is clear alongside the new `(Map-write probe)` companion.
Refs #12729.
* chore: Update package-lock.json with new dependencies and version upgrades
- Added new dependencies for @langchain/anthropic and @langchain/core, including @anthropic-ai/sdk and fast-xml-parser.
- Updated existing dependencies for @librechat/agents, @opentelemetry/api-logs, @opentelemetry/core, and related packages to their latest versions.
- Enhanced integrity checks and licensing information for new and updated packages.
* chore: Update @librechat/agents dependency to version 3.1.66 in package.json and package-lock.json
- Bumped the version of @librechat/agents from 3.1.65 to 3.1.66 across multiple package.json files to ensure consistency and access to the latest features and fixes.
* chore: Update dompurify and fast-xml-parser dependencies to version 3.4.0 and 5.6.0 respectively
- Bumped the version of dompurify across multiple package.json files to ensure consistency and access to the latest features and security fixes.
- Updated fast-xml-parser to the latest version in relevant package.json files for improved functionality.
* chore: Update @librechat/agents dependency to version 3.1.67 in package.json and package-lock.json
- Bumped the version of @librechat/agents from 3.1.66 to 3.1.67 across multiple package.json files to ensure consistency and access to the latest features and fixes.
* 🫧 fix: Restore Claude Opus 4.7 Reasoning Visibility
Claude Opus 4.7 omits `thinking` content from Messages API responses by
default — empty thinking blocks still stream, but the `thinking` field is
blank unless the caller passes `display: "summarized"` in the adaptive
thinking config. This silenced the LibreChat "Thoughts" UI for Anthropic
(and Anthropic-on-Bedrock) adaptive models.
- Extend `ThinkingConfigAdaptive` in `packages/api/src/types/anthropic.ts`
with an optional `display: 'summarized' | 'omitted'` field
- Emit `{ type: 'adaptive', display: 'summarized' }` from
`configureReasoning` in `packages/api/src/endpoints/anthropic/helpers.ts`
- Emit `{ type: 'adaptive', display: 'summarized' }` from
`bedrockInputParser` in `packages/data-provider/src/bedrock.ts` and
update the local `ThinkingConfig` union
- Update existing adaptive-thinking assertions to include the new field
- Add dedicated tests asserting `display: 'summarized'` flows through
both the Anthropic endpoint and the Bedrock parser
See https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7#thinking-content-omitted-by-default
* refactor: Gate `display: summarized` on Opus 4.7+
Narrow the reasoning-visibility opt-in to the models that actually omit
thinking content by default, instead of applying it to every adaptive
model. Pre-Opus-4.7 adaptive models (Opus 4.6, Sonnet 4.6) already return
summaries, so sending the field is unnecessary noise.
- Add `omitsThinkingByDefault(model)` in `packages/data-provider/src/bedrock.ts`
that returns true only for Opus 4.7+ (including future majors like Opus 5+)
- Bedrock parser now only attaches `display: 'summarized'` when the helper
matches, keeping the adaptive object unchanged for older models
- Anthropic endpoint `configureReasoning` uses the same helper so its emit
path matches the Bedrock one
- Tests: replace the blanket `display: 'summarized'` assertions with
model-specific ones (Opus 4.7 gets it, Opus 4.6 / Sonnet 4.6 do not),
add a dedicated `omitsThinkingByDefault` suite covering naming variants
and future versions
* feat: Configurable Thought Visibility for Anthropic Adaptive Models
Expose the Anthropic `thinking.display` API field as a user-facing
parameter so users can override the `auto` default (which stays as the
Opus-4.7+ opt-in added earlier in this PR). Also fixes the CI type error
by widening the adaptive thinking type assignment via a resolver helper
that returns a properly-typed object.
- Add `ThinkingDisplay` enum (`auto` | `summarized` | `omitted`) and
matching zod schema in `packages/data-provider/src/schemas.ts`
- Add `thinkingDisplay` to `tConversationSchema`, `anthropicSettings`, and
the pick lists for Bedrock input/parser + Anthropic agent params
- Add `resolveThinkingDisplay(model, explicit)` helper in
`packages/data-provider/src/bedrock.ts` that returns the wire value or
undefined (auto → model default, explicit → always honored)
- `bedrockInputParser` now reads `thinkingDisplay` from input and emits
`display` only when the resolver returns a value; strips the field
on non-adaptive-model branches so it does not leak
- `configureReasoning` in the Anthropic endpoint threads
`thinkingDisplay` through, uses the resolver, and casts the adaptive
config to `AnthropicClientOptions['thinking']` so the widened shape
compiles against the stale installed SDK types
- Add UI slider for `thinkingDisplay` in `parameterSettings.ts` next to
`effort`, with three-position `com_ui_auto` / `com_ui_summarized` /
`com_ui_omitted` labels
- Add translation keys `com_endpoint_anthropic_thinking_display`,
`com_endpoint_anthropic_thinking_display_desc`, `com_ui_summarized`,
`com_ui_omitted`
- Add tests: `resolveThinkingDisplay` suite (5 cases covering auto /
explicit / unknown input), parser round-trip tests for all three
modes on Opus 4.6 and Opus 4.7, Anthropic endpoint tests for explicit
summarized/omitted overrides
* fix: Drop `thinkingDisplay` When Adaptive Thinking Is Disabled
If a user turns adaptive thinking off but had previously selected a
`thinkingDisplay` value, the stale field was left in `additionalFields`
and ended up merged into the Bedrock request's
`additionalModelRequestFields`. That leaks a non-Bedrock key into the
payload and can round-trip back into `llmConfig`.
- Delete `additionalFields.thinkingDisplay` alongside `thinking` and
`thinkingBudget` in the `thinking === false` branch of
`bedrockInputParser`
- Add a regression test asserting `thinking`, `thinkingBudget`, and
`thinkingDisplay` are all absent when adaptive thinking is disabled on
an Opus 4.7 request
Reported by chatgpt-codex-connector on PR #12701.
* refactor: Consolidate `ThinkingDisplay` Types and Preserve Persisted Display
Address review findings on PR #12701:
- [Codex P2] `bedrockInputSchema.transform` now extracts
`thinking.display` from persisted `additionalModelRequestFields` back
into the top-level `thinkingDisplay` field so explicit `'omitted'`
round-trips through storage instead of being silently reverted to
`'summarized'` on the next parse.
- [Codex P2] `getLLMConfig` in the Anthropic endpoint now reads
`.display` from a persisted `thinking` object (agents store the full
Anthropic shape) and uses it as the fallback for `thinkingDisplay`
when no top-level override is present.
- [Audit #2] Collapse the three parallel wire-value types into a single
`ThinkingDisplayWireValue = Exclude<ThinkingDisplay, 'auto'>` exported
from `schemas.ts`; remove the duplicate `ThinkingDisplay` alias in
`packages/api/src/types/anthropic.ts` (which collided with the enum
name) and the `ThinkingDisplayValue` alias in `bedrock.ts`.
- [Audit #3] Add `thinkingDisplay` to the `TEndpointOption` pick list
next to `effort`.
- [Audit #4] Add a TODO comment next to the `as
AnthropicClientOptions['thinking']` cast explaining the stale
`@librechat/agents` SDK types that require it.
- Add tests: four round-trip cases asserting `bedrockInputSchema`
recovers `display` from persisted AMRF (Opus 4.7 omitted, pre-4.7
summarized, unknown-value ignore, explicit top-level wins), and two
`getLLMConfig` cases asserting the Anthropic endpoint preserves and
overrides persisted `thinking.display`.
* fix: Preserve Persisted `thinking.display` in bedrockInputParser
The parser constructed a fresh adaptive thinking config without looking at
any `display` already embedded in the incoming
`additionalModelRequestFields.thinking`. On round-trip through
`initializeBedrock`, a persisted user choice of `'omitted'` on Opus 4.7+
was silently reverted to `'summarized'` by the auto fallback.
- Extract `extractPersistedDisplay` helper and reuse it in both the
schema transform (form-state round-trip) and the parser (wire-request
round-trip)
- `bedrockInputParser` now feeds the persisted display as the resolver's
explicit value when no top-level `thinkingDisplay` override is set
- Add regression tests: parser preserves `display: 'omitted'` for
persisted Opus 4.7 AMRF, and top-level `thinkingDisplay` still wins
over persisted AMRF display
Reported by chatgpt-codex-connector (P1) on PR #12701.
* 🦉 feat: Claude Opus 4.7 Model Support
- Add `claude-opus-4-7` to shared Anthropic models and `anthropic.claude-opus-4-7` to Bedrock models
- Register 1M context window and 128K max output in anthropic token maps
- Add token pricing ($5/$25), cache rates (6.25/0.5), and premium tier ($10/$37.50 above 200K) in tx.ts
- Update `.env.example` with Opus 4.7 IDs in `ANTHROPIC_MODELS` and `BEDROCK_AWS_MODELS` examples
- Add parallel Opus 4.7 test cases for token/cache/premium rates, context length, max output, name-variation matching, and 1M-context qualification
* feat: Add `xhigh` Effort Level for Opus 4.7
- Add `xhigh` variant to `AnthropicEffort` enum between `high` and `max`
- Expose `xhigh` in `anthropicSettings.effort.options` and the UI slider `enumMappings`
- Reuse existing `com_ui_xhigh` translation key
* test: Cover `xhigh` Effort and Exact Opus 4.7 Premium Rates
- Assert `xhigh` position (between high and max), inclusion in
`anthropicSettings.effort.options`, zod acceptance, and rejection of
unknown values in schemas.spec.ts
- Verify bedrockInputParser emits `output_config: { effort: 'xhigh' }`
for adaptive `anthropic.claude-opus-4-7`
- Verify getLLMConfig sets adaptive thinking and `output_config.effort =
'xhigh'` for `claude-opus-4-7`
- Pin Opus 4.7 premium pricing to exact threshold/prompt/completion
values (200000 / 10 / 37.5) so silent rate drift fails the test
* refactor: Improve UX for Command Popovers
* Added loading state handling in Mention and PromptsCommand components to display a spinner when data is being fetched.
* Refactored onFocus logic to clear the textarea and set the search value based on command character input.
* Introduced a new `isLoading` state in the useMentions hook to manage loading indicators across multiple data queries.
* Added unit tests for the useHandleKeyUp hook to ensure command triggering works correctly under various conditions.
* ci: useHandleKeyUp tests for command navigation
* Added tests to ensure that the command popovers do not trigger when the cursor is mid-text after pressing ArrowLeft or Delete.
* Updated the shouldTriggerCommand function to refine the conditions under which commands are triggered based on cursor position.
* Improved agent query handling in useMentions hook for better performance and clarity.
* refactor: Optimize Mention and PromptsCommand Components
* Refactored Mention and PromptsCommand components to utilize Recoil state for popover visibility, improving state management and reducing prop drilling.
* Simplified onFocus logic to enhance user experience when interacting with command inputs.
* Added unit tests for useHandleKeyUp to ensure proper command handling and popover visibility based on user input.
* Improved performance by memoizing popover state and reducing unnecessary re-renders.
* fix: Address review findings for command popover refactor
- Fix endpointType regression: add effectiveEndpointByIndex selector
that returns endpointType ?? endpoint, matching the original ChatForm
guard for custom endpoints proxying assistants
- Extract duplicated initInputRef callback into shared useInitPopoverInput
hook, used by both Mention and PromptsCommand
- Add navigation keys (ArrowLeft, ArrowRight, ArrowDown, Home, End,
Delete) to invalidKeys to prevent false popover triggers
- Add endpoint gating tests for assistants/azureAssistants blocking the
+ command
- Remove unused _index param from MentionContent
* style: Update padding in ActionsPanel, ModelPanel, and AdvancedPanel for improved layout
* Adjusted padding in ActionsPanel, ModelPanel, and AdvancedPanel components to enhance visual consistency and layout.
* Changed `py-4` to `pt-2` in the main container of each panel to reduce vertical spacing and improve overall design aesthetics.
* style: Update text size in MCPTool component for improved accessibility
* Changed text size in the MCPTool component to `text-sm` for better readability and consistency across the UI.
* This adjustment enhances the user experience by ensuring that text is appropriately sized for various display settings.
* style: Enhance layout and accessibility in ActionsInput, ActionsPanel, and AgentPanel components
* Updated the layout in ActionsInput to improve flex properties and ensure better responsiveness.
* Refined the structure of ActionsPanel for a more consistent visual hierarchy and added accessibility features.
* Adjusted the AgentPanel form layout for improved usability and streamlined component integration.
* Changed text size in Dropdown component to `text-sm` for better readability across the UI.
* style: Refactor layout in ActionsInput component for improved responsiveness
* Updated the layout of the ActionsInput component to enhance flex properties and ensure better responsiveness.
* Adjusted the textarea styling for improved usability and consistency in design.
* Removed commented-out code related to example functionality to clean up the component structure.
* refactor: Simplify single line code detection in MarkdownComponents
* Introduced a new utility function `isSingleLineCode` to streamline the logic for determining if code is a single line.
* Updated references in the `MarkdownCode` and `MarkdownCodeNoExecution` components to use the new utility function for improved readability and maintainability.
* Enhanced the `processChildren` function in the Markdown editor to handle non-code elements more effectively.
* fix: handle `usage_metadata` in title transaction for Gemini models
Gemini models return token usage via `usage_metadata` instead of `usage`
or `tokenUsage`. The `collectedUsage` mapping in `titleConvo` only
handled the latter two, causing title generation transactions to be
silently skipped for Gemini. Adds an `else if (item.usage_metadata)`
branch to extract `input_tokens`/`output_tokens`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove trailing whitespace in usage_metadata handler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: normalize audio MIME types in STT format validation
Use getFileExtensionFromMime() to normalize non-standard MIME types
(e.g. audio/x-m4a, audio/x-wav, audio/x-flac) before checking against
the accepted formats list in azureOpenAIProvider. This is the same class
of bug as #12608 (text/x-markdown), but for STT audio validation.
Only audio/ and video/ MIME prefixes are normalized to prevent
non-audio types from matching via the webm default fallback.
Export getFileExtensionFromMime for testability.
Fixes#12632
* fix: reject unknown audio subtypes in STT format validation
Use MIME_TO_EXTENSION_MAP for normalization instead of
getFileExtensionFromMime() which falls back to 'webm' for unrecognized
types. Gate raw subtype matching on audio/video prefix to prevent
non-audio types (e.g. text/webm) from passing validation.
Resolves Codex review comment about unknown subtypes silently passing.
---------
Co-authored-by: Tobias Jonas <t.jonas@innfactory.de>
* fix: add docker system prune before image pull to prevent disk exhaustion
The 60GB droplet filled up after ~40 deploys because each
docker compose pull leaves the previous image's layers as
dangling/unused. The gitnexus image is ~700MB, so ~40 stale
copies ≈ 28GB of dead layers. Combined with indexes, OS, and
Docker's build cache, the disk hits 100% and the next pull fails
with 'no space left on device'.
Add a docker system prune -af --volumes BEFORE pulling the new
image on every deploy. This removes stopped containers, unused
networks, all images not referenced by a running container, and
build cache. Running containers are never touched. Typically
frees 1-2GB per deploy (the previous image's layers).
Also add a hard 2GB free-space guard after prune so the deploy
fails with a clear error instead of letting docker pull attempt
a 700MB extract onto a near-full disk.
* fix: cap PR indexes at 3 + delete-before-sync for 10GB disk
The 10GB droplet has ~2GB free. Each index is ~130MB, so 7 PR indexes
(~900MB) plus main+dev (~260MB) plus the ~700MB Docker image leaves
almost nothing for image pulls. The deploy failed with 'no space left
on device' during docker compose pull.
Three changes:
1. Cap PR indexes at MAX_PR_INDEXES=3. The resolve step now sorts
PR artifacts by created_at descending and only keeps the 3 most
recent. Older PR indexes are logged as evicted and their droplet
folders get cleaned by the prune step.
2. Prune BEFORE sync (was after). Freeing disk space from evicted
indexes before rsyncing new data is critical on a tight disk. The
old order (sync then prune) could briefly hold both old evicted
indexes and newly-uploaded ones simultaneously.
3. Delete-before-sync for every index, including main/dev. Instead
of rsync --delete (which transfers new files then removes extras),
rm -rf the target folder before rsync so the disk never holds both
old and new copies of the same index (~260MB saved per index).
Main/dev are only deleted when a fresh artifact is about to replace
them — never evicted between deploys.
Budget on 10GB disk:
OS + Docker engine: ~4.0 GB
Docker image (running): ~0.7 GB
main + dev indexes: ~0.26 GB
3 PR indexes: ~0.39 GB
Docker prune headroom: ~0.7 GB (for image pull)
Free: ~3.9 GB
* refine: restrict automatic PR indexing to danny-avila authored PRs
With 200+ open PRs and a 10GB disk capped at 3 served PR indexes,
auto-indexing every contributor PR burns CI minutes for artifacts
that will mostly be evicted before anyone queries them.
Narrow the pull_request auto-trigger to PRs authored by danny-avila
only. Other contributors' PRs can still be indexed on demand via
/gitnexus index (contributor-gated comment command) or manual
workflow_dispatch — both arrive as workflow_dispatch events and
bypass the pull_request filter entirely.
* fix: drop --volumes from docker system prune to preserve Caddy TLS state
The deploy workflow explicitly handles a caddy-not-running state later
in the same step. If Caddy is stopped when the prune runs, --volumes
deletes the caddy-data and caddy-config volumes (TLS certs + ACME
account keys), forcing a Let's Encrypt re-issuance on next start.
LE rate-limits to 5 certs per domain per week, so repeated wipes
could brick HTTPS for days.
docker system prune -af (without --volumes) still removes stopped
containers, unused networks, all dangling/unreferenced images, and
build cache — which is where the disk savings come from. Named
volumes are left untouched.
* fix: rsync-then-swap instead of delete-before-sync
The delete-before-sync pattern removed the live index BEFORE rsync
ran. If rsync failed (SSH timeout, disk pressure, network error),
the index was already gone — production served nothing for that
repo until a later deploy succeeded.
Replace with rsync-then-swap: upload to a .new temp directory, and
only rm + mv into place after rsync succeeds. On rsync failure,
the .new temp is cleaned up and the old index stays live. The cost
is ~130MB of extra disk while both old and new coexist, but the
prune step runs first and frees evicted PR indexes, so this fits
comfortably on the 10GB disk.
* fix: fail deploy on main/dev rsync failure, soft-fail PRs only
The rsync-then-swap pattern downgraded ALL failures to a warning,
so the deploy continued even when LibreChat or LibreChat-dev failed
to sync. The job would pull the new image, restart the container,
and report success while serving stale or missing core indexes.
Split by criticality: main/dev rsync failures now exit 1 (aborting
the deploy before the container restart). PR index failures remain
soft-fail with a warning — a missing PR index is inconvenient but
shouldn't take the whole server down.
* fix: endpoint token config not using shared cache in same process (initializing clients)
* refactor: Update default max context tokens for agent initialization
- Introduced a constant `DEFAULT_MAX_CONTEXT_TOKENS` set to 32000.
- Updated the `initializeAgent` function to use this constant instead of hardcoded values for maximum context tokens, improving maintainability and clarity.
* refactor: shared caching mechanism for token configuration
- Introduced a memoized in-memory cache for Keyv instances to ensure shared access across the same namespace, improving cache efficiency.
- Updated the `standardCache` function to utilize the new in-memory cache for the TOKEN_CONFIG namespace.
- Refactored the `initializeCustom` function to use the `tokenConfigCache` for better cache management.
- Removed redundant tokenCache parameter from `fetchModels` to streamline the function signature.
* fix: match TOKEN_CONFIG TTL and add memoization tests
Pass Time.THIRTY_MINUTES to tokenConfigCache() to match the TTL used
by getLogStores.js, preventing load-order-dependent expiry behavior.
Add 7 automated tests covering in-memory memoization: referential
identity, cross-call-site data sharing, namespace isolation,
first-caller TTL semantics, fallbackStore bypass, and tokenConfigCache
parity with direct standardCache access.
* fix: export DEFAULT_MAX_CONTEXT_TOKENS, address review nits
- Export the constant so tests (and future consumers) reference it
directly instead of hardcoding the numeric value.
- Add independent TTL assertion for tokenConfigCache (R-1 nit).
- Add tokenConfigCache mock to custom/initialize.spec.ts.
* 🔍 fix: Improve WebSearch Progress Handling Based on Attachment Results
- Adjusted progress handling in the WebSearch component to treat searches as complete if attachments contain results, addressing issues with server tool calls not receiving completion signals.
- Introduced `effectiveProgress` to reflect the actual state of progress based on the presence of results, enhancing the accuracy of cancellation and completion states.
- Updated related logic to ensure proper handling of search completion and finalization states based on the new progress calculations.
* fix: only override progress when not streaming
During streaming (isSubmitting=true), use actual progress so the
searching/processing/reading states display correctly. Only override
to 1 after streaming completes to prevent the cancelled check from
hiding the component.
* chore: Update @librechat/agents and mathjs dependencies to latest versions
* chore: Upgrade mathjs dependency to version 15.2.0 across package-lock and package.json files
* fix: preserve selected artifact when clicking artifact button
* fix: preserve artifact selection on click and during streaming
* fix: remove broken streaming guard, add JSDoc and test
- Remove `userHasManualSelection` guard from effect #3: it cannot
distinguish manual clicks from system auto-selection, blocking
auto-advancement to new artifacts during streaming.
- Add JSDoc on `currentArtifactIdRef` explaining why it must not be
added to effect deps (toggle-close regression).
- Add test verifying auto-advancement during streaming.
* style: use standard multi-line JSDoc format for ref comment
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: clear stale client registration on invalid_client during token refresh
When a token refresh fails with `invalid_client`, the stored DCR client
registration is no longer valid on the authorization server. The existing
error handler only checked for `unauthorized_client` and returned null,
leaving the stale client_id cached in the database permanently. Every
subsequent token refresh attempt would fail with the same error.
Now when `invalid_client` is detected during refresh:
1. The stale client registration is deleted from the database
2. A `ReauthenticationRequiredError` is thrown to trigger a fresh OAuth
flow with new dynamic client registration
Also passes `deleteTokens` from MCPConnectionFactory to getTokens() so
the cleanup has access to the token deletion method.
* fix: address review findings for stale client cleanup on token refresh
- Delete stale refresh token alongside client registration on invalid_client (Finding 1)
- Add tests for all new code paths: cleanup, warning, case-insensitivity, cleanup failure (Finding 2)
- Detect all vendor-specific client rejection patterns (client_id mismatch, client not found, unknown client) with case-insensitive matching (Finding 3)
- Use else-if for mutually exclusive error branches (Finding 4)
- Log warning when deleteTokens is not available on client rejection (Finding 6)
- Fix log message to say "attempting to clear" before async cleanup (Finding 7)
- Extract isClientRejectionMessage to shared utility, refactor MCPConnectionFactory.isClientRejection to use it
* fix: address followup review findings
- Extract isInvalidClientMessage (4 stale-client patterns) from isClientRejectionMessage
to eliminate pattern duplication between utils.ts and tokens.ts (Finding 1)
- Remove redundant staleIdentifier variable, reuse identifier already in scope (Finding 2)
- Separate await from .then() on Promise.allSettled for readability (Finding 3)
- Add dedicated unit tests for isInvalidClientMessage and isClientRejectionMessage (Finding 4)
- Assert error message content in primary invalid_client test (Finding 5)
- Add JSDoc on deleteTokens in GetTokensParams (Finding 6)
---------
Co-authored-by: Mani Japra <mani@muonspace.com>
* 🗂️ feat: Sidebar Icon Toggle & New Chat History Switch
Add collapse-on-active-click for sidebar icons (VSCode-style) and optionally switch to Chat History panel when creating a new chat.
* fix: Address review findings — extract DEFAULT_PANEL constant, add tests
Export DEFAULT_PANEL from ActivePanelContext and use it in ExpandedPanel
instead of hardcoding 'conversations'. Add ExpandedPanel tests covering
NavIconButton collapse toggle and NewChatButton panel switch behaviors.
* fix: Address review — prop-drill setActive, test disabled setting, strengthen assertions
Pass setActive as a prop to NewChatButton instead of subscribing to
ActivePanelContext, avoiding wasted re-renders on every panel switch.
Add negative-path test for switchToHistory=false. Add positive panel
assertions to inactive-icon click tests. Fix import order.
* 🐛 fix: resolve Action tools by exact tool name to prevent multi-action collision
When two OpenAPI Actions on the same Agent share a hostname, the second
action's entry overwrote the first in the encoded-domain Map and one
action's tools silently disappeared from the LLM payload. The buggy
resolution loop also used substring matching, which caused similar
shadowing for any encoded-domain prefix overlap.
This change builds a Map keyed on the full tool name
(`<operationId>_action_<encoded-domain>`) directly, mirroring the exact
lookup pattern that getActionToolDefinitions already uses. Each function
in an action's spec gets its own slot, so two actions sharing a hostname
no longer collide. Both the new and the legacy domain encodings are
registered for each function so agents whose stored tool names predate
the current encoding still resolve.
Applied at all three call sites that had the buggy pattern:
- processRequiredActions (assistants/threads path)
- loadAgentTools (agent build path)
- loadActionToolsForExecution (agent execution path)
Adds three regression tests covering both ordering directions and the
execution path. Tests fail without the fix and pass with it.
* 🐛 fix: Normalize action tool name at lookup + cover assistants path
Follow-up to the multi-action domain collision fix. Addresses PR #12594
review feedback:
**Must-fix #1 — short-hostname lookup mismatch.** The toolToAction map
is keyed on the `_`-collapsed domain, but `agent.tools` and
`currentAction.tool` persist the raw `domainParser(..., true)` output,
which for hostnames ≤ ENCODED_DOMAIN_LENGTH is a `---`-separated string
(e.g. `medium---com`). Exact-match `Map.get()` missed those keys and
silently dropped the tool. Fix: normalize every incoming tool name
through a new `normalizeActionToolName` helper before the lookup in
`loadAgentTools`, `processRequiredActions`, and
`loadActionToolsForExecution`.
**Must-fix #2 — assistants path coverage.** `processRequiredActions`
received the same structural rewrite but had zero tests. Added a
regression test under `multi-action domain collision regression` that
drives two shared-hostname actions through the assistants path and
asserts each tool reaches its own request builder.
**Must-fix #3 — legacy encoding branch coverage.** The
`if (legacyNormalized !== normalizedDomain)` registration was never
exercised by any test. Added a test where `agent.tools` stores the
legacy-format name and asserts it still resolves.
**Should-fix #4 — DRY the registration loop.** Extracted
`registerActionTools({ toolToAction, functionSignatures,
normalizedDomain, legacyNormalized, makeEntry })`. All three call sites
now share the same key-building logic; the key template lives in one
place.
**Should-fix #5 — remove stale optional chaining.** In
`loadActionToolsForExecution`, `functionSignature?.description ?? ''`
became `functionSignature.description` — `sig` is always defined by the
iterator, matching the style of `loadAgentTools`.
**Should-fix #6 — drop unreachable `!requestBuilder` guard.** Entries
in `processRequiredActions` are now pre-built with
`requestBuilder: requestBuilders[sig.name]`, which `openapiToFunction`
always produces alongside the signature, so the guard is dead.
**Should-fix #7 — unwrap `actionSetsData`.** It now holds a bare
`Map` instead of `{ toolToAction }`; the sentinel `!actionSetsData`
check still works because `new Map()` is truthy.
Also added a short-hostname regression test
(`loadAgentTools resolves raw ---separated tool names`) that reproduces
Must-fix #1: it fails against the previous commit (0 create calls) and
passes with the normalization in place.
41 tests, all passing. The 3 new regression tests are under
`multi-action domain collision regression` and cover the assistants
path, the legacy encoding branch, and the short-hostname lookup path.
* 🐛 fix: Tighten registerActionTools key handling and assistants test
Follow-up to d643444 addressing the second review pass on PR #12594.
**ESLint** — Two prettier errors in the spec file (multi-line arrow
function bodies that should fit on one line). Auto-fixed.
**[MINOR] operationId containing `---` → key mismatch.** The lookup
path collapsed every `actionDomainSeparator` sequence in the full tool
name, but the registration path passed `sig.name` through unchanged.
A `---` that survived into an operationId would shift the underscore
boundary at lookup and miss its own key. Fix in `registerActionTools`:
normalize `sig.name` with the same helper so registration and lookup
always agree on the canonical form. `sanitizeOperationId` strips the
characters that produce `---` in practice, so this is theoretical
hardening, not a fix for a known reproducer.
**[MINOR] Same-operationId + same-hostname silent overwrite.** Two
actions sharing both an operationId and a hostname still produced a
silent `Map.set()` overwrite (the new key is identical, so neither the
operationId nor the domain disambiguates). Added a `setKey` helper
inside `registerActionTools` that logs a `[Actions] operationId
collision: ...` warning whenever a key is already present, naming the
overwriting action_id. The silent-overwrite mode from the original bug
cannot reappear under a different disguise without surfacing in the
logs.
**[NIT] processRequiredActions test simulated a runtime crash.**
`mockCreateActionTool` returned a tool with `_call: jest.fn()`, which
resolves to `undefined`. `processRequiredActions` chains
`.then(handleToolOutput).catch(handleToolError)` directly onto that
return, so `undefined.then(...)` threw synchronously and the outer
try/catch funneled the error into `handleToolError`. Creation count
assertions still passed because `createActionTool` runs before the
crash, but the test was silently exercising the failure path. Updated
the global mock to `_call: jest.fn().mockResolvedValue('{"status":"ok"}')`
so the success path runs end-to-end. The assistants regression test
now executes in ~5ms instead of ~90ms, which corroborates that it's
no longer hitting the synchronous throw.
**[NIT] Duplicated rationale comments.** All three call sites carried
multi-line comment blocks restating why we key on the full tool name.
That rationale now lives canonically in `registerActionTools`'s JSDoc;
the inline blocks collapsed to `// See registerActionTools for the
key-shape rationale.` Net -22 lines of comments.
41/41 tests still pass; lint is clean.
* 🐛 fix: Scope tool-name normalization to the encoded-domain suffix
Follow-up to f22228e addressing the Codex P1 on PR #12594.
**Regression.** The previous commit normalized the entire tool name
(`normalizeActionToolName(sig.name)` at registration, full-name
`.replace()` at lookup) to handle operationIds that theoretically
contained `---`. But `openapiToFunction` uses user-supplied
operationIds verbatim and the fallback `sanitizeOperationId` only
strips characters outside `[a-zA-Z0-9_-]`, so specs can legitimately
produce operationIds like `get_foo---bar` and `get_foo_bar` side by
side. Collapsing `---` to `_` across the entire key merged those two
into a single map slot — one silently overwrote the other, and both
tool requests routed to the surviving entry's request builder.
**Fix.** Limit normalization to the encoded-domain portion of the
full tool name, i.e. the substring after the last `actionDelimiter`.
The operationId half is left verbatim, so hyphens-vs-underscores
remain disambiguating. The short-hostname bug (Must-fix #1 from the
original review) is still covered because the `---` → `_` collapse
still happens on the domain suffix where it matters:
```js
const normalizeActionToolName = (toolName) => {
const delimiterIndex = toolName.lastIndexOf(actionDelimiter);
if (delimiterIndex === -1) return toolName;
const prefixEnd = delimiterIndex + actionDelimiter.length;
const encodedDomain = toolName.slice(prefixEnd);
return toolName.slice(0, prefixEnd) + encodedDomain.replace(domainSeparatorRegex, '_');
};
```
`registerActionTools` reverts to `sig.name` verbatim — no more
`normalizeActionToolName(sig.name)` ahead of the key build.
**Regression test.** Added
`loadAgentTools distinguishes operationIds that differ only by
---` vs `_`` under `multi-action domain collision regression`. It
loads two actions sharing a hostname whose operationIds are
`get_foo---bar` and `get_foo_bar` respectively, each pointing at a
different path (`/foo-bar`, `/foo_bar`), and asserts that each tool
resolves to its own request builder. Verified the test fails against
f22228e (`hyphenTool` resolves to `/foo_bar` — the sibling's builder)
and passes with this commit.
42/42 tests pass; lint clean.
* feat: implement optimized Entra group sync with auto-creation
## Changes
### MUST FIX (Critical Issues) - RESOLVED
1. **BUG FIX: Prevent unintended user removal from existing groups**
- ISSUE: db.syncUserEntraGroups() was called with only missing groups, causing removal
from all existing Entra groups (full bidirectional sync behavior)
- SOLUTION: Replaced with db.upsertGroupByExternalId() for each missing group followed
by single bulkUpdateGroups() to add memberships (race-safe, idempotent)
- BENEFIT: User memberships correctly maintained for mix of existing + new groups
2. **JSDoc @throws contradiction**
- ISSUE: JSDoc declared function throws, but implementation catches all errors
- SOLUTION: Removed @throws from JSDoc - function is best-effort
- BENEFIT: Prevents unnecessary try/catch in caller code
3. **Missing test for group creation flow**
- ISSUE: Auto-creating missing Entra groups had no test coverage
- SOLUTION: Added regression test for mix of existing + new groups scenario
- BENEFIT: Prevents future regressions on critical path
### SHOULD FIX (Important Improvements) - RESOLVED
4. **E11000 race condition handling**
- SOLUTION: Upserts are idempotent and race-safe by design
- BENEFIT: Concurrent logins no longer race each other
5. **Direct Mongoose access instead of db layer**
- SOLUTION: Added findGroupsByExternalIds() helper to userGroup.ts
- BENEFIT: Centralized data access, easier to add tenant scoping
6. **Serial DB round-trips on login path**
- ISSUE: 40+ queries for user with 20 new groups
- SOLUTION: Promise.all() for parallel upserts + single bulkUpdate
- BENEFIT: ~10x performance improvement
7. **Graph API 429/503 throttling unhandled**
- SOLUTION: Retry logic with exponential backoff (1s, 2s delays)
- BENEFIT: Temporary API issues no longer cause permanent membership loss
8. **Sequential batch requests slow**
- ISSUE: 200 groups = 10 batches × 200ms = ~2s sequential
- SOLUTION: Promise.all() with concurrency limit (5 parallel batches)
- BENEFIT: ~400ms total time
## Minor Fixes
- Removed dead code check
- PII removal: user._id instead of user.email in logs
- ES6 shorthand fixes
- Style consistency (blank lines)
- Projection optimization
## Verification
✅ npm run build - success
✅ npm run test:api - 61/61 passing (+ new regression test)
✅ npm run lint - no errors
✅ All feedback from danny-avila resolved
* docs: better JSDoc for the syncUserEntraGroupMemberships method
---------
Co-authored-by: Airam Hernández Hernández <airam.hernandez@intelequia.com>
* 🔬 fix: Scope Web Search Results to Own Turn in WebSearch Component
Fix duplicated search results when multiple web_search tool calls occur
in a single response. Each WebSearch instance now displays only its own
turn's results instead of aggregating all turns.
* test: Add WebSearch turn-scoping tests
Cover the regression-prone turn isolation logic: verify each WebSearch
instance renders only its own turn's sources when sharing a SearchContext,
test the attachments-first priority path and the searchResults fallback,
and assert component states (cancelled, error, streaming, complete).
* fix: Use getAllByText for duplicate sr-only + visible text in tests
WebSearch renders status text in both an sr-only aria-live region and
a visible span, causing getByText to fail with multiple matches.
* refactor: Make ownTurn a string to avoid repeated conversions
Three related changes that tighten the GitNexus CI/CD loop.
Serialized deploys
- Previous concurrency group was keyed by head ref with cancel-in-progress,
which let deploys targeting different refs (e.g. main push + PR command)
run in parallel. That's a data race: the prune-stale-indexes step
computes active_names up front, so deploy A rsyncing
/opt/gitnexus/indexes/LibreChat-pr-12580 can collide with deploy B
pruning the same folder based on a pre-rsync view of the active set.
- Collapse to a single global group gitnexus-deploy with
cancel-in-progress: false. All deploys queue behind one another.
A rsync/docker-compose restart is never killed mid-operation.
The 20-minute job timeout bounds queue depth.
PR completion feedback
- Add a "index complete" comment step in gitnexus-index.yml that
fires only when inputs.pr_number is set (i.e. the run came via the
/gitnexus command). Posts success or failure with a link to the
run and whether embeddings were generated.
- Add a "deploy complete" comment step in gitnexus-deploy.yml that
handles both trigger paths: workflow_run from a native PR auto-index
(PR number recovered from the matrix entry whose runId matches the
trigger run), and workflow_dispatch from the index workflow's bot-
fallback path (PR number passed through as a new inputs.pr_number).
- Plumb inputs.pr_number through the bot-fallback dispatch in
gitnexus-index.yml so the deploy workflow knows where to comment
for command-triggered runs.
- Only comments on the PR that asked for the index, never broadcasts.
Workflow rename
- Drop the "DigitalOcean" suffix from the deploy workflow's display
name and filename. The platform is still DO (.do/gitnexus/ still
holds the compose + caddy config) but the workflow itself is
platform-agnostic in form and the suffix was visual noise.
- File renamed gitnexus-deploy-do.yml -> gitnexus-deploy.yml.
- Concurrency group and all cross-references updated in lock-step.
- permissions at deploy job level now includes pull-requests: write
so the completion comment can post.
* fix: dispatch deploy from index when triggered by github-actions[bot]
GitHub Actions suppresses workflow_run events for workflow runs whose
triggering actor is GITHUB_TOKEN (to prevent recursive chains). This
means when gitnexus-pr-command.yml uses `gh api workflow_dispatch`
to kick off gitnexus-index.yml, the downstream gitnexus-deploy-do.yml
workflow_run trigger never fires — the PR command indexes the PR but
the new artifact never makes it onto the droplet.
Add a final step in gitnexus-index.yml that dispatches the deploy
workflow directly via API, but ONLY when the triggering actor is
github-actions[bot]. User-triggered runs (push, pull_request, manual
workflow_dispatch from the UI) continue to rely on workflow_run as
before, so we don't double-deploy.
Requires a new actions:write permission at the workflow level for
this dispatch. contents:read is unchanged.
* fix: resolve main/dev indexes by artifact name, not branch run query
The resolve step was querying listWorkflowRuns filtered by branch=main
and branch=dev, then assuming the latest successful run on each branch
produced the expected gitnexus-index-main / gitnexus-index-dev
artifact. That assumption breaks for /gitnexus index command runs:
The PR command workflow dispatches gitnexus-index.yml with ref=main
(because that's where the workflow file lives) and an input pr_number.
The resulting run has head_branch='main' but uploads its artifact as
gitnexus-index-pr-<N>, not gitnexus-index-main. listWorkflowRuns
returns that run as the "latest success on main", the download step
tries to fetch gitnexus-index-main from it, and the API returns
"no artifact matches any of the names or patterns provided".
Fix: resolve all indexes (main, dev, and PRs) through the same
listArtifactsForRepo path the PR discovery already uses. Looks up
the freshest non-expired artifact by name directly, so the run's
head_branch and event type don't matter — if the artifact exists,
we find it; if not, we warn and move on.
Side benefit: the resolution logic is now shorter and consistent
across branches and PRs.
* fix: paginate open PRs and parallelize artifact lookups
The resolve step was capped at 100 open PRs by github.rest.pulls.list's
per_page ceiling — LibreChat has 200+ open at any given time, so the
tail of the PR queue was silently skipped. On top of that, the inner
artifact lookup loop was serial, so even after pagination the resolve
step would take 40-60 seconds on a busy repo (one API call per PR).
- Replace the single-page rest.pulls.list call with github.paginate,
which follows the Link header across pages and returns the full
open-PR set regardless of count.
- Drop the 100-PR truncation warning that was a known-limitation
notice for exactly this case.
- Batch the per-PR artifact lookups into groups of 10 via Promise.all.
200 PRs now take ~10 seconds instead of ~60, and the burst stays
well within the authenticated rate limit (5000/hr).
- Add a final core.info summary showing how many of the open PRs
actually had a servable index artifact, so the log is useful for
debugging why a specific PR isn't showing up on the droplet.
* feat: auto-enable embeddings for dev and PR indexes too
Previously only main branch pushes got --embeddings; dev and
contributor PRs ran graph-only and relied on BM25 search. Semantic
search on those indexes silently returned empty, which defeats the
whole point of serving them to MCP clients.
New logic: every automatic trigger (push to main/dev, pull_request
from contributors) enables --embeddings. Only workflow_dispatch
still respects the explicit input toggle, so operators can run a
fast graph-only re-index when they don't need fresh vectors.
Cost: adds ~3-5 minutes per index run. Acceptable tradeoff for
having semantic search work across all served branches + open PRs
instead of just main.
* refine: gate PR embeddings on unit-test path relevance
Previous version auto-enabled --embeddings on every contributor PR,
which cost ~3-5 min of extra CI per index even on PRs that couldn't
benefit from semantic code search (docs, config, workflow files,
i18n strings, etc.).
New logic mirrors the backend-review.yml and frontend-review.yml
path filters — if a PR doesn't touch api/, client/, or packages/
it won't trigger unit tests and it doesn't need embeddings. The
check queries the GitHub API for the PR's changed file list via
`gh api repos/.../pulls/<N>/files` (paginated for very large PRs)
and enables embeddings only when at least one path matches.
main/dev pushes still always embed. workflow_dispatch still respects
the explicit input toggle, which also covers the /gitnexus index
[embeddings] PR command.
The contributor gate at the job level is unchanged — non-contributor
PRs are still skipped entirely regardless of paths.
* feat: /gitnexus command works for non-contributor and fork PRs
The command workflow already gated on the commenter's author
association (not the PR author's), so a contributor commenting
/gitnexus index on an outside contributor's PR passes the auth
check. But the downstream index workflow checked out the PR's
raw head SHA, which only exists in the fork for cross-repo PRs —
actions/checkout fetches from the base repo's origin and fails.
Switch the command workflow to dispatch with refs/pull/<N>/head
instead of the SHA. GitHub mirrors every PR's head into the base
repo as this ref regardless of whether the PR is from a fork, so
the checkout always resolves.
End result: a contributor can type `/gitnexus index embeddings`
on any PR — including one opened by a first-time contributor from
a fork — and the index (with embeddings, if requested) is built
and served. The contributor takes responsibility for the trust
boundary by typing the command.
Updated the relevant header/inline comments in both workflows so
the next maintainer understands the refs/pull/<N>/head choice and
the commenter-based gating.
* refine: /gitnexus index defaults to embeddings on
A contributor typing the command has already chosen to spend ~5
minutes of CI on a full re-index; they wouldn't invoke the command
just to get a BM25-only result. Flip the default so the short form
`/gitnexus index` produces an embeddings-enabled index.
Modifier semantics:
/gitnexus index -> embeddings ON (new default)
/gitnexus index embeddings -> embeddings ON (explicit, no-op alias)
/gitnexus index fast -> embeddings OFF (opt-out)
/gitnexus index graph-only -> embeddings OFF (alias)
/gitnexus index no-embeddings-> embeddings OFF (alias)
The previous `embeddings` modifier is preserved as a no-op alias so
anyone who learned the earlier form still gets what they expected.
The first GHCR build on main failed with:
ERROR: Cache export is not supported for the docker driver.
The default docker driver on ubuntu-latest runners can't export
cache to type=gha. docker/setup-buildx-action@v3 without a driver
argument defaults to docker-container, which supports both
cache-from and cache-to. Gated on the same condition as the build
step so it only runs when an image rebuild is actually needed.
* feat: migrate GitNexus deployment from Fly.io to DigitalOcean droplet
Fly.io's 1GB machine was pegged at ~900MB memory with load spiking to
2.7 under even modest query load. Moving to a 2GB+ DO droplet that can
take advantage of existing credits.
Architecture change: indexes no longer baked into the image. Instead,
a long-lived image (built only when .do/gitnexus/ changes) is pulled
from GHCR, and the deploy workflow rsyncs .gitnexus/ data into
/opt/gitnexus/indexes/<name>/ on the droplet and restarts only the
gitnexus container. Caddy stays running so TLS certs don't churn.
- Add .do/gitnexus/Dockerfile (same native-addon + extension patch
layers as the Fly variant, but no COPY indexes/ step)
- Add .do/gitnexus/docker-compose.yml with gitnexus + caddy services
on an internal bridge network, 1.8GB memory limit, healthcheck
- Add .do/gitnexus/Caddyfile with automatic HTTPS for the configured
subdomain and bearer token auth for all routes except /health
- Add .do/gitnexus/entrypoint.sh that registers every index mounted
at /indexes/<name>/.gitnexus at container start, then runs
gitnexus serve bound to 0.0.0.0 (internal docker network only)
- Add .do/gitnexus/install-extensions.js for LadybugDB FTS/vector
extension pre-install (workaround for upstream bug)
- Add .github/workflows/gitnexus-deploy-do.yml that builds the image
only on Dockerfile/entrypoint changes, pushes to GHCR, rsyncs the
index artifacts to the droplet, and restarts the gitnexus container
- Remove .fly/gitnexus/ and .github/workflows/gitnexus-deploy.yml —
Fly app will be destroyed after DO deploy is verified working
Required new secrets: DO_HOST, DO_USER, DO_SSH_KEY. GITNEXUS_DOMAIN
and API_TOKEN live in /opt/gitnexus/.env on the droplet itself.
* refactor: prefix deploy secrets with GITNEXUS_ for namespace isolation
Rename DO_HOST -> GITNEXUS_DO_HOST, DO_USER -> GITNEXUS_DO_USER, and
DO_SSH_KEY -> GITNEXUS_DO_SSH_KEY so the secrets are clearly scoped
to the gitnexus deploy and don't collide with any other DigitalOcean
secrets LibreChat might add later.
* feat: serve PR indexes alongside main/dev and add /gitnexus command
The index workflow was already building and uploading per-PR indexes
(gitnexus-index-pr-<N>) for contributor PRs, but the deploy workflow
only consumed main and dev artifacts. PR indexes were sitting in
storage doing nothing. This wires them all the way through to the
live MCP server, with proper cleanup when PRs close.
Deploy workflow changes:
- Drop the branches filter on workflow_run so PR index completions
also trigger deploys (PR indexes are already contributor-gated
upstream in gitnexus-index.yml via author_association)
- Resolve all open PRs via the GitHub API, look up each one's latest
non-expired gitnexus-index-pr-<N> artifact, and serve whichever
ones exist. PRs without an index artifact are skipped — we don't
retroactively index anything.
- Per-ref concurrency group so rapid pushes to the same PR coalesce
but different refs still deploy in parallel
- After rsyncing active indexes, prune any /opt/gitnexus/indexes/
folder that isn't in the active set. Safety net for missed PR
close events.
New workflow: gitnexus-cleanup-pr.yml
- Fires on pull_request closed (merged or not)
- SSHs to the droplet, removes /opt/gitnexus/indexes/LibreChat-pr-<N>,
restarts the gitnexus container
New workflow: gitnexus-pr-command.yml
- Listens for issue_comment events where body starts with /gitnexus
- Contributor gated via author_association
- Supports: /gitnexus index — index with defaults
/gitnexus index embeddings — index with --embeddings
- Dispatches gitnexus-index.yml with the PR number and head SHA,
reacts to the comment with a rocket emoji
Index workflow changes:
- New dispatch inputs pr_number and pr_ref for command-driven runs
- Checkout step uses inputs.pr_ref when set so the PR's head commit
is analyzed instead of the default branch
- Artifact naming falls back through pr_number -> pull_request number
-> ref_name, keeping existing behavior for push/PR events
- Concurrency group switches to pr-<N> when dispatched by the command
so re-runs on the same PR debounce correctly
* chore: remove Fly variant reference from Dockerfile header
The Fly variant no longer exists in the repo, so the comparison
comment is meaningless. Rewritten as a standalone description of
the image's design.
* review: resolve 15 findings from review audit
Critical
- Drop the unused caddy binary from the gitnexus image. Caddy runs in
its own container in this architecture; installing it inside the
gitnexus image added ~40-60MB for no reason and contradicted the
Dockerfile header comment.
Major
- Replace ssh-keyscan TOFU with a GITNEXUS_DO_KNOWN_HOST secret.
Both deploy and cleanup workflows now pin the droplet's host key
from a stored value instead of silently trusting whatever the host
presents at deploy time. Fails the workflow if the secret is empty
so no one accidentally regresses to TOFU.
- Gate gitnexus-cleanup-pr.yml to same-repo PRs via
github.event.pull_request.head.repo.full_name == github.repository.
Fork PR closes no longer produce failed runs when secrets are
withheld by GitHub. The deploy workflow's stale-folder prune step
remains the safety net for any fork-contributor indexes.
- Fail fast in entrypoint.sh when main/dev index registration errors.
Previously `|| echo WARN` swallowed failures so a broken index
passed the docker healthcheck and the deploy was marked green
while queries returned empty. PR indexes stay best-effort
(a corrupt PR index shouldn't take the whole server down).
- Authenticate the droplet with GHCR on every deploy using
GITHUB_TOKEN, so private GHCR packages work without documentation
detours or manual docker login on the host. Bootstrap comments
explain the flow.
- Switch docker-compose caddy.depends_on from the short-form
(service_started) to service_healthy so Caddy doesn't route to a
gitnexus container that's still starting (500ms-60s window after
recreation).
Minor
- Guard the HEAD~1 diff with `git rev-parse --verify HEAD~1` so the
first-commit and workflow_run-from-PR cases default to rebuild
instead of silently skipping a legitimately-changed image.
- Move `packages: write` off the workflow-level permissions and onto
the build-image job. deploy no longer inherits unnecessary GHCR
write access.
- Skip the SSH session in cleanup-pr.yml when no gitnexus-index-pr-<N>
artifact ever existed for the PR. Eliminates ~95% of no-op SSH
round-trips on a busy repo (docs-only PRs, paths-ignored PRs, etc).
- Reload Caddy in-place after config upload with `caddy reload`,
falling back to force-recreate on reload failure and `compose up`
on first-time bootstrap. Picks up Caddyfile or env changes without
losing TLS certs.
- Replace `sleep 5` post-deploy with a real readiness poll against
docker's health status. Fails the workflow if gitnexus doesn't
report healthy within 120s, so a broken startup surfaces in CI
instead of being silently marked green.
- Warn when listPulls hits the 100-item per_page ceiling so a future
growth spurt past 100 open PRs doesn't silently drop indexes.
Nit
- Tighten NODE_OPTIONS --max-old-space-size from 1536 to 1280MB,
giving KuzuDB's C++ heap ~512MB of room under the 1792MB cgroup
limit instead of ~256MB.
- Rewrite the stale "headroom for Caddy" comment in entrypoint.sh
(Caddy lives in a separate container now).
- Restore load-bearing comments in install-extensions.js explaining
the @ladybugdb/core path layout and the throwaway-db cache-priming
pattern.
- Parameterize the docker-compose image reference as
${GITNEXUS_IMAGE:-ghcr.io/danny-avila/librechat-gitnexus:latest}
so forks or pinned version tags can override via /opt/gitnexus/.env.
Deferred
- Finding 12 (memory headroom) addressed partially via the heap cap
reduction; full profiling of KuzuDB C++ allocations under query
load deferred to post-deploy monitoring.
* review: resolve 8 follow-up findings from second review pass
Security
- F1: pipe GHCR token via SSH stdin instead of expanding it into the
remote command string. Previously `"echo '$GH_TOKEN' | docker login"`
expanded the token locally before SSH sent it as an argument, so the
live token was briefly visible in /proc/<pid>/cmdline on the droplet
to any process running as deploy or root. New form uses
`printf '%s' "$GH_TOKEN" | ssh ... "docker login --password-stdin"`
so the token only travels through the encrypted SSH stdin pipe.
Reliability
- F2: add json-file log rotation (50m x 3 files) via a YAML anchor
shared by both services. Default Docker logging is unbounded and
would eventually fill the 60GB droplet disk.
- F4: set memswap_limit=1792m to match mem_limit. Without this, Docker
lets the container silently spill onto host swap when KuzuDB's C++
heap overruns the 1792m RAM budget, turning sub-second graph queries
into multi-second ones with no alert. Hard OOM-kill is preferable —
unless-stopped restarts the container, the deploy health poll
catches it, the failure is explicit.
- F5: extend the post-deploy health poll from 24 iterations (120s) to
36 iterations (180s) so it clears Docker's own unhealthy-detection
ceiling (start_period 60s + retries 3 * interval 30s = 150s). A
container that legitimately takes 125s to warm up would previously
fail the deploy at 120s while Docker would still report it as
"starting".
Operability
- F3: document the `--no-deps` escape hatch in the compose file header
so an operator can restart Caddy during a gitnexus outage without
being trapped by the service_healthy dependency (e.g. emergency
Caddyfile fix while gitnexus is thrashing).
- F7: rewrite the misleading service_healthy comment. The old text
said it prevents 502s "after a restart", implying continuous
protection. Clarified that depends_on only governs initial compose
up ordering — during force-recreates Caddy briefly routes to a
starting gitnexus and the deploy's health poll is the actual guard.
- F6: add `shopt -s nullglob` before the prune loop so an empty
/opt/gitnexus/indexes directory is an explicit no-op instead of
relying on the quirk that `rm -rf "*"` (with literal "*") silently
succeeds. Next reader won't have to recognize the bash default.
- F8: soft-fail PR artifact downloads when the artifact disappeared
between resolve and download. Main/dev artifact failures stay fatal
because a missing main/dev index is a real deploy failure, but a
deleted PR artifact no longer aborts the whole deploy.
* review: resolve 3 NITs from third review pass
- F1: rewrite the printf '%s' comment. The previous version claimed
docker login --password-stdin rejects trailing newlines, which is
inaccurate — docker login strips whitespace. The real reason for
printf over echo is byte-exact output and portability, and the
token-in-process-table security rationale is already documented
in the preceding sentences.
- F2: when a PR artifact download soft-fails, the PR's name stays
in active_names so the prune step keeps the droplet's existing
copy instead of wiping it (stale > empty). Make this transition
visible by spelling it out in the ::warning:: message.
- F3: fencepost fix in the health poll. The previous loop ran 36
iterations and claimed "180s" in the comment, but the final
iteration exits without a trailing sleep, so the real ceiling
was 35 * 5s = 175s. Extended to 37 iterations (36 sleeps * 5s
= 180s) so the comment matches reality.
text/x-markdown is a deprecated version of a markdown mimetype, but
we're seeing that sometimes users still send this mimetype. This
change allows these files to be uploaded as text/markdown.
The published gitnexus npm package compiles pool-adapter.ts to
dist/mcp/core/lbug-adapter.js, not dist/core/lbug/pool-adapter.js
as I guessed in the previous PR. The sed patch failed the Docker
build because the target file didn't exist.
Point the patch at the correct compiled path and add a grep -c
verification after sed to confirm the replacement actually landed.
Two upstream GitNexus 1.5.3 bugs combine to break query() in serve mode:
1. pool-adapter.ts calls LOAD EXTENSION fts but never INSTALL fts. The
CI-produced .gitnexus/ artifact doesn't include the extension cache
(~/.kuzu/extension/), so LOAD silently fails in the fresh container
and all BM25/FTS searches return empty.
2. pool-adapter.ts only loads the FTS extension — it never loads the
vector extension. Every CALL QUERY_VECTOR_INDEX in semanticSearch
fails, so hybrid search's semantic leg returns empty too.
Combined, query() returns empty even for exact function names because
both the BM25 and semantic legs of RRF merging have zero inputs.
Meanwhile context()/cypher()/route_map() still work because they use
plain Cypher with no extensions.
Workaround:
- Add install-extensions.js that runs INSTALL fts and INSTALL vector
against a throwaway database during Docker build, populating
~/.kuzu/extension/ with the cached extension binaries
- Sed-patch pool-adapter.js at build time to also LOAD EXTENSION vector
alongside the existing FTS load, wrapped in try/catch
- Update deploy workflow to copy install-extensions.js into the build
context
* fix: capture gitnexus serve output and add startup health check
gitnexus serve was backgrounded with no log capture, so crash
reasons were invisible in Fly logs. Now pipes output to stdout
and waits up to 30s for the server to be ready before starting
Caddy, with early exit if the process dies.
* fix: install newer libstdc++ for LadybugDB native addon
@ladybugdb/core prebuilt binary requires GLIBCXX_3.4.32 (GCC 13+)
but node:24-slim ships Bookworm's libstdc++6 which only has 3.4.31.
Pull libstdc++6 from Debian Trixie to satisfy the runtime dependency.
* fix: separate build tools from libstdc++ upgrade to avoid conflict
Installing Trixie's libstdc++6 alongside Bookworm's g++ fails because
the Trixie libc6 transitive dep conflicts with Bookworm's libc6-dev.
Split into two RUN steps: compile native addons first, remove g++,
then upgrade libstdc++ from Trixie with no conflicting packages.
* fix: name repo as LibreChat and add dev branch deploy support
- Use REPO_NAME build arg (default: LibreChat) as the WORKDIR so
gitnexus registers the index with a proper name instead of "repo"
- Deploy workflow now triggers on both main and dev branch index runs
- Dev branch registers as "LibreChat-dev", main as "LibreChat"
- workflow_dispatch gains a branch selector input
* feat: serve both main and dev indexes from one container + auto-embed main
- Dockerfile now copies multiple indexes from indexes/<name>/.gitnexus
and registers each with gitnexus, so one server handles both branches
- Deploy workflow downloads latest successful main + dev artifacts in
parallel and bundles them into a single deploy
- list_repos returns LibreChat and LibreChat-dev; queries target either
via the repo parameter
- Main branch pushes auto-enable --embeddings for semantic search;
dev and PRs remain graph-only for speed (opt-in via dispatch input)
- Bump index job timeout to 25m to account for embedding generation
* fix: raise Fly machine memory to 1GB + add swap + cap Node heap
The 512MB machine was getting OOM-killed when gitnexus serve's default
--max-old-space-size=8192 over-committed memory during query spikes.
- Bump VM memory 512mb -> 1gb
- Add 512MB swap file to absorb transient spikes
- Cap Node heap at 768MB via NODE_OPTIONS so it stays within the
machine's real capacity and leaves headroom for Caddy and the OS
The default <Button> variant (bg-primary / text-primary-foreground)
renders invisible in light mode on the Agent Marketplace 'Start Chat'
button and the Grant Access dialog 'Save Changes' button, making these
primary actions undiscoverable without hovering.
Switch these three affirmative-action buttons to variant='submit',
which uses the hardcoded 'bg-surface-submit text-white' combination
already defined in the Button component specifically to avoid the
contrast issues of the default variant (see the comment above the
'submit' variant in packages/client/src/components/Button.tsx).
No visual change in dark mode; in light mode the buttons now render
as the green submit color with white text, matching the semantic
intent of the action.
Co-authored-by: Timothy Look <timothy.look@pmv.eu>
* feat: Enhance favorites functionality to support model specs
* refactor(FavoritesList): reorder imports based on lenght
* feat: improve favorite modelSpec controller; refactor: useIsActiveItem hook
* refactor: consolidate Favorite type, harden controller, add tests
- Add canonical TUserFavorite type in data-provider, replace three
duplicate definitions (data-service, data-schemas, store/favorites)
- Consolidate FavoritesController spec validation into single block,
add return on 500 paths, add maxlength to mongoose sub-schema
- Fix import order in FavoritesList.tsx, merge namespace type imports
in FavoriteItem.tsx and FavoritesList.tsx
- Add focus-visible:ring-inset on pin buttons to prevent ring clipping
- Add explicit return type and JSDoc on useIsActiveItem hook
- Use props.type for narrowing consistency in FavoriteItem getTypeLabel
- Add 22 backend tests for FavoritesController (spec validation,
typeCount exclusivity, persistence, GET path)
- Add 40 frontend tests: useFavorites spec methods, useIsActiveItem
observer lifecycle, ModelSpecItem pin button, FavoriteItem all three
type branches, FavoritesList spec rendering
* fix: address PR review findings for pin model specs
- Harden backend validation to reject partial cross-type fields
(e.g. spec+endpoint, agentId+model without endpoint)
- Add stale-spec auto-cleanup in FavoritesList mirroring agent cleanup
- Add type="button" to pin buttons in ModelSpecItem/EndpointModelItem
- Fix import order violations in EndpointModelItem and ModelSpecItem
- Remove hollow test, dead key prop, inline trivial helpers
- Fix misleading test description, add onSelectSpec to test mock
- Add return to controller success responses for consistency
- Add 6 backend tests for partial cross-type field validation
* fix: guard stale-spec cleanup against unloaded startupConfig
Prevents race condition where spec favorites are incorrectly deleted
on cold start before startupConfig has loaded. Mirrors the existing
agentsMap === undefined guard pattern used for stale agent cleanup.
Also adds tests for stale-spec cleanup persistence and fixes namespace
import pattern in FavoritesList.spec.tsx.
* fix: replace nested ternaries with if/else in FavoriteItem
Resolves ESLint no-nested-ternary warnings for name and typeLabel
derivations.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: respect supportedMimeTypes config in file picker accept filter
The browser file picker's accept attribute was hardcoded by provider
identity, ignoring the endpoint's supportedMimeTypes from fileConfig.
Users who configured permissive MIME types (e.g., '.*') still saw a
restrictive filter in the upload dialog.
Add isPermissiveMimeConfig utility that detects wildcard patterns in
the endpoint's supportedMimeTypes. When permissive, the file picker
accept attribute is set to empty (unrestricted). Non-permissive
configs retain the existing provider-based defaults.
Closes#12589
* fix: address review findings for isPermissiveMimeConfig
- Use non-standard MIME namespace probe (x-librechat/x-probe) so
category-wildcard patterns like ^application\/.*$ no longer
false-positive as permissive
- Add single-line JSDoc to isPermissiveMimeConfig
- Use !== undefined instead of != null (fileType is never null)
- Add endpointFileConfig to dropdownItems useMemo deps to prevent
stale closure when config changes without endpoint change
- Add tests for broad application and multi-category patterns
* fix: wrap handleUploadClick in useCallback to satisfy exhaustive-deps
handleUploadClick is captured inside the dropdownItems useMemo but was
not in its dependency array. Wrap it in useCallback with
endpointFileConfig.supportedMimeTypes as the sole dependency, then
reference the stable callback in the useMemo deps.
* refactor: Add user job tracking TTL to RedisJobStore
- Introduced a new TTL for per-user job tracking sets, set to 24 hours, to enhance job management.
- Updated RedisJobStoreOptions interface to include userJobsSetTtl for configuration.
- Modified job creation and deletion methods to manage user job sets effectively, ensuring proper expiration and cleanup.
- Enhanced comments for clarity on the new TTL functionality and its implications for user job tracking.
* fix: Address review findings for user job tracking TTL
- Remove redundant `del(userJobsKey)` in `getActiveJobIdsByUser` that
raced with concurrent `createJob` on other replicas (Redis auto-deletes
empty Sets after SREM)
- Guard `userJobsSetTtl: 0` from silently destroying tracking sets
(`EXPIRE key 0` deletes the key on Redis 7.0+)
- Extract `deleteJobInternal` so `cleanup()` reuses the already-fetched
userId instead of issuing a redundant HGETALL per stale job
- Add integration tests for TTL behavior, proactive SREM, configurable
userJobsSetTtl, and TTL refresh on repeated createJob
* fix: Address follow-up review findings for RedisJobStore
- Use deleteJobInternal in cleanup() terminal-but-in-running-set path
to ensure userJobsKey SREM is not skipped
- Clear local caches in deleteJob before the fallible getJob call so
they are cleaned even on transient Redis errors
- Add proactive SREM tests for aborted and error terminal statuses
- Add test for tenant-qualified user tracking key format
* fix: Preserve completedTtl for non-running jobs in cleanup()
The cleanup() terminal-status branch should only remove tracking set
membership, not delete the job hash. deleteJobInternal bypasses the
completedTtl window that updateJob already applied, causing clients
polling for final status to lose the job data early.
upload-artifact@v4 defaults include-hidden-files to false,
which silently skips the .gitnexus/ directory (dotfile).
Scoped to the .gitnexus/ path so only index files are affected.
* fix: atomize Redis event sequence counters for multi-replica deployments
Replace in-memory sequenceCounters Map with shared atomic Redis counter
(INCR via Lua script) so all replicas share an authoritative sequence
source. Make syncReorderBuffer async to read current sequence from Redis
instead of defaulting to 0 on non-publishing replicas.
Closes#12575
* fix: harden emitChunk error handling and syncReorderBuffer race safety
Wrap getNextSequence inside emitChunk's try/catch so a transient Redis
failure is logged-and-swallowed, preserving the non-fatal chunk emission
contract. In syncReorderBuffer, replace pending.clear() with selective
discard of stale entries (seq < currentSeq) followed by
flushPendingMessages, preventing loss of chunks that arrived during the
async GET window.
* fix: address review findings — harden error paths, validate types, DRY tests
- Wrap syncReorderBuffer in try/catch in GenerationJobManager.subscribe()
so a Redis GET failure degrades gracefully instead of crashing SSE reconnect.
- Validate eval return type in getNextSequence; throw on unexpected type
instead of silently computing NaN/-1 and dropping all messages.
- Add NaN guard to parseInt in syncReorderBuffer for corrupted Redis values.
- Consolidate two streams loops in destroy() into a single pass.
- Extract createMockPublisher to shared helpers/publisher.ts (DRY).
- Add tests for eval failure and unexpected eval return type in emitChunk.
- Document performance tradeoff (2x RTT per emit) and TTL rationale.
* test: add cross-replica integration tests for sequence desync fix (#12575)
Three real-Redis tests that reproduce the exact multi-replica failure:
- Late subscriber on a different replica syncs to the shared counter
and receives chunks immediately (no 500ms force-flush).
- Multiple subscribe/unsubscribe cycles across replicas maintain
correct sequence alignment on every reconnect.
- Shared counter key is cleaned up when the stream is destroyed.
* fix: close syncReorderBuffer race, stop destroy() from nuking shared keys
- Replace selective prune with Math.min(currentSeq, minPendingSeq) so
chunks that arrive in pending during the async GET window are preserved
instead of incorrectly pruned. Since unsubscribe() already clears
pending, any entries at sync time are live messages from the current
subscription and must not be discarded.
- Remove DEL from destroy() — the sequence key is shared across replicas
and a shutting-down instance must not nuke it for active publishers.
Keys expire naturally via their 1-hour TTL; cleanup() handles
single-stream lifecycle teardown.
- Add race-condition test: pauses GET, injects a message into pending
during the window, resolves GET, asserts the chunk is delivered.
- Add emitDone/emitError eval failure tests to cover the asymmetric
error contract (chunk swallows, done/error propagates).
- Add explicit MockPublisher return type to helpers/publisher.ts.
* fix: context-aware syncReorderBuffer to prevent duplicate delivery, silence test logs
The Math.min(currentSeq, minPending) fix from the prior commit correctly
handles the cross-replica race (chunk arriving during async GET), but
causes duplicate delivery in same-replica mode: onAbort subscribes to
the Redis channel during createJob, so chunks published before
subscribe() may arrive via pub/sub AND earlyEventBuffer. The Math.min
logic then flushes the pub/sub copy as a "live" message.
Fix: add a clearPending parameter to syncReorderBuffer. The manager
passes true when earlyEventBuffer was replayed (same-replica: pending
entries are duplicates → clear them) and false/undefined when it was
not (cross-replica: pending entries are live → preserve via Math.min).
- Silence winston logger in stream test files via logger.silent = true,
following the existing pattern in data-schemas/prompt.spec.ts.
- Remove sequence key DEL from destroy() — shared across replicas,
a shutting-down instance must not nuke the counter for active
publishers. Keys expire via TTL; cleanup() handles stream teardown.
* fix: share publisher client in cross-replica tests for Redis Cluster compat
The cross-replica tests created publisherB via (ioredisClient as Redis)
.duplicate(), which produces a plain Redis connection to a single node.
In Redis Cluster, GET/EVAL on this client can't follow MOVED redirects,
so syncReorderBuffer reads null → nextSeq=0 → chunks are buffered.
Fix: share ioredisClient as the publisher for both replicas. It's the
correct Cluster-aware client and handles slot routing automatically.
Only subscriber connections need to be separate (pub/sub requirement).
* fix: increase cross-replica test timeouts and use polling for CI cluster
Replace fixed 300ms delivery waits with polling (50ms intervals, 2s max)
to handle Redis Cluster's cross-node pub/sub broadcast latency in CI.
Increase subscription activation wait from 100ms to 500ms to match the
pattern used by existing same-instance tests.
* chore: silence winston logs in remaining stream test files
Add logger.silent = true to GenerationJobManager, RedisJobStore, and
collectedUsage test files, matching the pattern already applied to
RedisEventTransport and reconnect-reorder-desync tests.
* fix: remove flaky cluster pub/sub tests, fix log suppression for resetModules
Remove two cross-replica transport-level integration tests that are
inherently non-deterministic in Redis Cluster: cluster pub/sub fan-out
is async across nodes, so pre-subscribe PUBLISHes can arrive at the
subscriber's node after SUBSCRIBE takes effect, causing random +/- 1
message counts. The core logic is already covered by deterministic unit
tests (mock publisher) and GenerationJobManager integration tests
(end-to-end with earlyEventBuffer). The cleanup test is retained.
Switch log suppression from logger.silent (which doesn't survive
jest.resetModules) to jest.spyOn(console, 'log').mockImplementation()
for files that use resetModules (GenerationJobManager, RedisJobStore,
collectedUsage). The logger.silent approach remains for files that
don't use resetModules (RedisEventTransport, reconnect-reorder-desync).
* fix: replace Lua EVAL+TTL with plain INCR, preserve live chunks during same-replica sync
P1: syncReorderBuffer with clearPending=true was unconditionally clearing
pending, dropping NEW chunks (seq >= currentSeq) from ongoing generation
that arrived via pub/sub during the async GET. Fix: selectively prune
only entries with seq < currentSeq (duplicates of earlyEventBuffer) and
flush remaining live entries.
P2: The 1-hour TTL on sequence keys could expire mid-stream during long
quiet periods (e.g., slow tool calls), restarting the counter from zero
and causing subscribers to silently drop all subsequent messages. Fix:
remove the Lua EVAL+EXPIRE script entirely and use plain Redis INCR —
no TTL. Keys are cleaned up explicitly by cleanup()/resetSequence()
when streams end. Orphaned keys from crashed processes are a few bytes
each, negligible compared to the production risk of TTL expiry.
- Update mock publisher helper: eval → incr
- Remove "unexpected eval type" tests (not applicable to incr)
- Update remaining eval error tests to reference incr
* fix: re-arm flush timeout after syncReorderBuffer when gaps remain
syncReorderBuffer clears flushTimeout before processing, but if pending
still has gaps after flushPendingMessages, no timeout is re-armed.
Without new messages arriving to trigger scheduleFlushTimeout, those
buffered entries sit indefinitely — stalling the stream after reconnect.
* chore: address final review — fix stale docs, rename clearPending, tighten tests
Fix all 10 findings from final review pass:
F1: Fix stale TTL comment in destroy() — no TTL exists after EVAL removal
F2: Fix stale EVAL reference in emitChunk JSDoc → INCR + PUBLISH
F3: Fix syncReorderBuffer JSDoc — describes old broken behavior, not
the current selective prune
F4: Rename clearPending → pruneStaleEntries for clarity at call sites
F5: Add publish-not-called assertion to INCR failure test
F6: Replace Math.min(...spread) with explicit loop to avoid heap alloc
F7: Remove resetSequence from IEventTransport interface (no external
callers; implementation detail only)
F8: Consolidate cleanup/resetSequence overlap — cleanup() owns the DEL,
resetReorderBuffer() (now private) handles buffer state only
F9: Fix import order in reconnect test (value before type imports)
F10: Export MockPublisher interface from helpers/publisher.ts
* chore: fix stale resetSequence references and hadBufferReplay JSDoc
Two comments referenced resetSequence() which was removed in the F7/F8
refactor (now private resetReorderBuffer(), which doesn't DEL the key).
Only cleanup() deletes the Redis key. Also update hadBufferReplay JSDoc
to say "prune stale entries" instead of the pre-refactor "clear pending".
* chore: grammar
* fix: use earlyReplayCount as prune cutoff instead of Redis counter
The boolean pruneStaleEntries flag used currentSeq (from Redis GET) as
the prune threshold, but INCR can advance the counter past a live
chunk's seq during the GET window. Example: earlyEventBuffer held seqs
0-4, generation emits seq 5 during GET, INCR advances counter to 6,
GET returns 6, prune condition 5 < 6 deletes the live chunk.
Fix: pass the earlyEventBuffer replay count (5) as the prune cutoff.
Entries with seq < earlyReplayCount are true duplicates; entries at or
above are live regardless of what the Redis counter reads. After
pruning, the unified Math.min(currentSeq, minPending) logic handles
both same-replica and cross-replica paths correctly.
Add a targeted test that exercises this exact race: pauses GET, emits
seq 5 during the window, resolves GET with counter=6, asserts seq 5
is preserved (would have been dropped with the old boolean approach).
* fix: pass earlyReplayCount for skipBufferReplay path to prune pub/sub duplicates
When skipBufferReplay is true (resume scenario), earlyEventBuffer events
are delivered via the resume sync payload, not replayed directly. But
earlyReplayCount was left at 0, so syncReorderBuffer treated all pending
entries as live — meaning delayed pub/sub copies of those buffered
events could be delivered again, duplicating content.
Fix: capture earlyEventBuffer.length before the skip/replay branch so
the count is always passed to syncReorderBuffer regardless of delivery
method. Seqs 0..earlyReplayCount-1 are pruned as duplicates whether
they were replayed or delivered via sync payload.
* fix: keep nextSeq monotonic in syncReorderBuffer after async GET
handleOrderedChunk can deliver in-order messages and advance nextSeq
during the async GET window. If those messages leave pending empty,
the unconditional nextSeq = currentSeq could regress nextSeq below
its already-advanced value, reopening a delivered gap and causing
subsequent messages to be buffered until force-flush.
Fix: wrap both nextSeq assignments with Math.max(nextSeq, ...) so
syncReorderBuffer never moves the delivery frontier backward.
* fix: cap nextSeq at earlyReplayCount, add 24h safety TTL, add post-GET race test
Finding 1 (CRITICAL): When earlyReplayCount > 0 and pending is empty,
nextSeq was set to currentSeq — but INCR can advance the counter past
a live chunk whose pub/sub hasn't arrived yet. Cap at earlyReplayCount
instead (what was actually delivered), so in-flight chunks are not
skipped. Adds test for the message-arrives-AFTER-GET-resolves scenario.
Finding 3: Add a 24-hour safety-net TTL set once on first INCR only
(val === 1), never refreshed. This caps orphan lifetime from crashed
processes without risking mid-stream counter resets.
Finding 6: Replace setTimeout(100) in cleanup test with polling loop.
Finding 9: Fix variadic DEL mock + add expire mock for the new TTL.
Revert P2 fix (earlyReplayCount for skipBufferReplay path) — when
skipBufferReplay is true, the resume sync payload delivers everything
up to currentSeq, so syncReorderBuffer should trust the Redis counter
as the frontier, not the buffer length.
* chore: log expire failures consistently with other fire-and-forget errors
* fix: Merge Custom Endpoints by Name Instead of Replacing Entire Array
The DB base config's `endpoints.custom` array was wholesale-replacing
the YAML-derived array, causing YAML endpoint additions to be silently
lost after the first admin panel save. Add path-aware array merging
to `deepMerge` so keyed arrays (matched by `name`) are merged item-by-item
instead of replaced.
* fix: Harden mergeArrayByKey — deduplicate, sanitize, and prevent mutation
- Remove redundant sourceOrder array; iterate Map.keys() instead to
prevent duplicate entries when source contains repeated names.
- Sanitize override-only appended items through deepMerge to enforce
UNSAFE_KEYS prototype-pollution protection on all code paths.
- Shallow-copy unmatched base items to prevent mutation leak-back.
- Add post-OVERRIDE_KEY_MAP remapping note to ARRAY_MERGE_KEYS JSDoc.
- Add tests: duplicate source names, base mutation safety, multi-priority
sequential merging of the same custom endpoint.
* fix: Add inline comments and test for keyless source items
- Document keyless item drop behavior in mergeArrayByKey with inline
comment and matching test case.
- Add last-write-wins comment to deduplication test assertion.
- Clarify path semantics in mergeArrayByKey target-iteration comment.
* fix: Relocate keyless-item comment and test target-side preserve
- Move keyless-item comment to the if-guard where the skip happens and
clarify that target-side keyless items are preserved, not dropped.
- Add test verifying base items without a name field are kept in output.
* style(sidebar): polish button styles, icon sizes, and nav separator
- Match new chat button style to sidebar toggle
- Unify icon sizes and stroke weights across sidebar icons
- Add separator between new chat and nav links
feat(prompts): inline prompt editing with route-based navigation
- Show prompts dashboard inline when editing/creating from chat
- Use route-based navigation instead of state-driven view switching
- Strip inline view to form-only without duplicate sidebar
style(prompts): unify borders, backgrounds, and admin controls
- Unify borders to border-medium and remove opaque backgrounds
- Add admin/advanced controls to sidebar
- Match chat background, move advanced toggle to versions panel
refactor(prompts): consolidate list actions into dropdown with compact layout
- Unify prompt list to single ChatGroupItem with all actions
- Consolidate actions into dropdown menu
- Distinct dropdown icons with status icon tooltips
- Remove rename action, use Trash icon matching convo items
refactor(prompts): inline-edit title with click-to-edit and save indicator
- Click title text to enter edit mode with pencil icon on hover
- Clean title input with inline save status indicator
- Uniform h-9 header height, smaller title, icon-only save status
- Remove separate edit icon button and confirm/cancel controls
style(prompts): sticky click-to-edit pill with surface-primary background
fix: preserve AuthContext identity across Vite HMR updates
createContext() was re-executed on every HMR module replacement, creating
a new context object that disconnected the existing provider from its
consumers. useAuthContext() would throw because useContext(newContext)
returned undefined while the provider was still on the old context.
Stash the context object in import.meta.hot.data so it survives HMR
re-execution. Dead-code-eliminated in production builds.
feat: add advanced prompts editor toggle to Chat settings
- Add AdvancedPrompts switch in Settings > Chat that maps the
promptsEditorMode enum (simple/advanced) to a boolean toggle
- Preserve existing behavior: switching to simple forces alwaysMakeProd
- Add focus styling to PromptName input (border-medium on focus,
outline on focus-visible, no ring)
- Add localization keys: com_nav_advanced_prompts,
com_nav_advanced_prompts_desc
- Various Prompts UI refinements
style(sidebar): update nav icons and reorder links for clarity
- Prompts: MessageSquareQuote → NotebookPen (distinct from chat bubble)
- Agent Builder: Blocks → Bot (no longer identical to Assistants)
- Memories: Database → BrainCircuit (brain + AI circuit)
- Parameters: Settings2 → SlidersHorizontal (tuning-specific)
- Reorder: Chat → Prompts → Agents → Assistants → Memories →
Bookmarks → Files → Parameters → MCP (frequency and grouping)
refactor(prompts): remove AdvancedSwitch component
The simple/advanced toggle is now in Settings > Chat, making
the standalone AdvancedSwitch redundant. Remove the component,
its barrel exports, and all render sites (PromptForm,
DashBreadcrumb, PromptsView).
style(prompts): tighten panel layout and spacing to match sidebar conventions
- Remove top margin from form wrapper, add mt-2 to header row
- Reduce header gaps and remove flex-wrap for tighter alignment
- Shrink prompt title from text-lg/h-9 to text-base/h-8
- Add pl-2 inner padding to PromptName input and static button
- Compact versions panel and deploy button container padding
- Reduce editor header and content spacing
- Compact mobile versions panel header
fix(prompts): rebuild mobile versions panel with proper transitions
- Add background (bg-surface-primary-alt) and border-l so content
doesn't bleed through
- Replace conditional render with CSS opacity/translate so both
overlay fade-out and panel slide-out animate on close
- Use inert attribute to disable focus when panel is hidden
- Replace raw × character with X icon from lucide-react
- Use size-icon button variant for close instead of text sm
- Use Tailwind classes (w-80, translate-x-full) instead of inline
style object for width and transform
- Remove unused sidePanelWidth constant
- Overlay stays in DOM with pointer-events-none when hidden
chore: remove stale tooling artifacts
Remove .bg-shell/manifest.json and .gsd symlink that were
accidentally committed — these are local dev tooling files.
fix: resolve review issues in PromptName and ChatGroupItem
- Fix Escape/onBlur race in PromptName via cancelledRef guard
- Clear existing timer before creating new one to prevent status flicker
- Guard delete mutation against empty group id
- Attach menuButtonRef to MenuButton for proper focus restoration after preview dialog closes
fix: address review findings across prompts UI and sidebar
PromptName:
- Fix Enter key double-save: set skipBlurRef before inline save to prevent
onBlur from re-firing saveName on input unmount
- Add isError prop to distinguish mutation failure from success, preventing
false 'saved' checkmark on error
- Remove dead cn ternary that always evaluated to opacity-100
- Remove unused cn import
PromptForm:
- Pass isError from updateGroupMutation to PromptName
- Remove dead default arg (= {}) on function component
ChatGroupItem:
- Stabilize handleDelete via ref pattern to prevent memo-defeating
recreation on every render (deleteGroup is a new ref each render)
- Disable delete button during mutation to prevent double-fire
ExpandedPanel:
- Restore <a> element for NewChatButton to fix middle-click (open in
new tab) regression caused by anchor-to-button migration
DashGroupItem:
- Delete dead file (no longer exported or imported anywhere)
fix: address review findings across prompts UI and sidebar
- PromptName: render error state (red X) when save fails, extract
shared commitName helper to deduplicate blur/Enter save paths
- ChatGroupItem: navigate away after deleting the active prompt in
sidebar view; use context-aware route prefix (/prompts vs /d/prompts)
for edit and card-click navigation
- InlinePromptsView: redirect to /c/new when user lacks prompts access
instead of rendering a blank screen
- Remove dead ManagePrompts component and its barrel exports (no
remaining consumers after GroupSidePanel cleanup)
fix: remove duplicate showThinking Recoil atom key
The 'showThinking' key was defined in both store/settings.ts (Recoil)
and store/showThinking.ts (Jotai). Only the Jotai atom is consumed;
the stale Recoil duplicate causes 'A key option with a unique string
value must be provided' at startup.
refactor: remove /d/prompts dashboard route and dead code
The prompts UI now lives at /prompts/* inline under the chat layout.
The old /d/prompts/* dashboard route, its layout (PromptsView), and
its breadcrumb (DashBreadcrumb) are no longer used.
- Delete PromptsView and DashBreadcrumb (zero consumers)
- Delete BackToChat button (zero consumers)
- Replace /d/prompts/* child routes with a redirect to /prompts/new
- Add /prompts index route that redirects to /prompts/new
- Update all /d/prompts navigation to /prompts:
- ChatGroupItem: always use /prompts prefix
- NoPromptGroup: navigate to /prompts
- CreatePromptForm: fallback navigate to /prompts/:id
- CreatePromptButton: simplified to /prompts/new (no dual-path)
- Strip GroupSidePanel of dashboard-only breadcrumb nav, recoil
state clearing, and useDashboardContext dependency
refactor: remove dead Dashboard code and unused translation keys
- Delete DashboardContext provider (zero remaining consumers)
- Simplify DashboardRoute layout to a plain Outlet
- Remove commented-out file/vector-store route blocks
- Change catch-all redirect from /d/files to /c/new
- Remove 7 unused translation keys (com_nav_toggle_sidebar,
com_ui_back_to_chat, com_ui_dashboard, com_ui_delete_prompt_name,
com_ui_global_group, com_ui_prompt_renamed, com_ui_rename_prompt,
com_ui_rename_prompt_name)
fix: add Babel plugin to transform import.meta.hot for Jest
`babel-plugin-transform-import-meta` handles standard properties (url,
filename, dirname, resolve) but not Vite's `hot` property. Jest runs in
CommonJS where `import.meta` is unavailable, so `import.meta.hot` in
AuthContext.tsx (added for HMR preservation) causes a SyntaxError that
breaks 27 test suites.
Add a small Babel plugin that replaces `import.meta.hot` with `undefined`
during Jest transforms, making the HMR guard blocks dead-code in tests.
fix: address review findings — inert typing, accessibility, and cleanup
- Add React type augmentation for `inert` attribute (React 18 compat)
- Replace spread hack `{...{ inert: }}` with direct prop in all 3 files
- Add `aria-hidden` to mobile overlay in PromptForm for screen readers
- Simplify deleteGroupRef pattern to direct mutation call
- Remove unused `useEffect` import and stale useMemo dependency
- Add clarifying comment for skipBlurRef mechanism in PromptName
fix: mobile UX for marketplace and prompts views
- Remove mobile new chat button from chat history section
- Add OpenSidebar entry points to marketplace and prompts views on mobile
- Move marketplace admin settings to a compact mobile top row
- Restructure prompt forms to surface category selector beside sidebar toggle
- Make versions panel slide content like the main sidebar and drop redundant borders
- Collapse versions button to icon-only on mobile
- Remove theme selector from prompt panel navigation
* fix: address PR review findings for prompts refactor
- Preserve prompt ID in /d/prompts/:id → /prompts/:id redirect
- Gate PreviewPrompt and VariableDialog behind isChatRoute to avoid
mounting dead dialogs in dashboard mode
- Add onError handler to useDeletePromptGroup and close dialog on
success
- Use useId() instead of hardcoded labelId in AdvancedPrompts
- Extract shared lazy loader for InlinePromptsView routes
* fix: complete review fixes for prompts refactor
- Move OGDialog (delete) inside isChatRoute gate with other dialogs
- Use useId() for both Switch id and label id in AdvancedPrompts
- Add com_ui_prompt_delete_error i18n key for actionable error context
- Drop no-op useCallback on handleDelete (unstable deleteGroup dep)
* refactor: hoist promptPath to module-scope constant
Eliminates stale-closure lint concern in dropdownItems useMemo and
removes the unnecessary dep array entry from onCardClick.
* refactor(sidebar): update icons and reorder links for clarity
- Replace Blocks icon with OpenAIMinimalIcon for the Assistant Builder link.
- Update Memories icon from BrainCircuit to Brain.
- Reintroduce Prompts link conditionally based on access permissions.
- Change Conversations icon from MessageSquare to MessagesSquare for consistency.
* refactor(sidebar): update icons and improve file attachment link
- Replace NewChatIcon with SquarePen in the NewChatButton for better visual consistency.
- Change AttachmentIcon to Paperclip in the file attachment link for clarity.
* refactor(sidebar): update file attachment icon for consistency
- Replace Paperclip icon with AttachmentIcon in the file attachment link for improved clarity and visual consistency.
* refactor(admin-settings): remove unused button and streamline dialog integration
- Eliminate the Admin button and its associated icon from the AdminSettings component for a cleaner interface.
- Simplify the confirm dialog integration by directly using the OGDialog without the button trigger.
* fix: context HMR issue
* style(prompts): enhance component structure and accessibility
- Update AutoSendPrompt button class for improved styling.
- Refactor List component to streamline loading and empty states.
- Ensure FilterPrompts handles context gracefully with null checks.
- Modify GroupSidePanel to prevent rendering without context.
- Simplify PromptsAccordion layout for better readability.
- Adjust CategoryIcon fallback behavior for undefined categories.
* refactor(useMCPServerManager): clean up import statements
- Remove duplicate import of MCPServerInitState for better clarity and organization.
- Adjust import order to maintain consistency with project structure.
* refactor(GroupSidePanel): restructure layout for improved readability and accessibility
- Adjust the structure of the GroupSidePanel component to enhance layout clarity.
- Move the PanelNavigation component into a more appropriate position within the hierarchy.
- Ensure consistent styling and behavior based on the isChatRoute condition.
* style(GroupSidePanel): adjust padding for improved layout consistency
- Update padding in the GroupSidePanel component to enhance visual alignment and readability.
- Ensure consistent styling across the component for a better user experience.
* fix(Conversations): add cache clearing and row height recomputation on search query change
- Implement useEffect to clear cache and recompute row heights when the search query changes.
- Enhance performance and responsiveness of the Conversations component during search operations.
* refactor(GroupSidePanel, PromptsAccordion): simplify layout and improve styling
* chore: import order
* fix: redirect users without CREATE permission from /prompts/new
Users with USE but not CREATE permission were seeing a blank page at
/prompts/new because InlinePromptsView only checked USE access.
CreatePromptForm's internal redirect was bypassed by the onSuccess
prop always being passed. Add CREATE check in InlinePromptsView so
the redirect happens before CreatePromptForm mounts.
* fix: restore dropdown actions for all routes and handle non-creator landing
- Remove isChatRoute gate on dropdown menu so preview, edit, and
delete actions are available on the prompts management route
- Un-gate PreviewPrompt and OGDialog (delete) since both are
triggered from the now-always-visible dropdown
- Keep VariableDialog gated behind isChatRoute (chat submission only)
- Show EmptyPromptPreview for non-creators at /prompts/new instead
of redirecting to /c/new, so they stay in the prompts section
with sidebar access to browse existing prompts
* fix: add isPublic to TPromptGroup type
The database schema (IPromptGroup in data-schemas) has isPublic but
the shared TPromptGroup type in data-provider was missing it,
causing a TS2339 error in ChatGroupItem.
* fix: prevent duplicate rename and restore name on error in PromptName
- Block re-entry to edit mode while a save is in flight by guarding
the click handler with isLoading/saveStatus checks
- Reset newName to the prop value when mutation fails so the UI
doesn't display the unsaved name after the error icon clears
* fix: address review findings across prompts refactor
- Consolidate duplicate usePromptGroupsContext() calls in PromptForm
- Remove invalid aria-labelledby (text string, not ID) from
AutoSendPrompt checkbox that is already aria-hidden
- Remove useMemo wrapping trivial `disabled ?? false` in ToolsDropdown
- Remove dead context spread in PromptsAccordion (GroupSidePanel
reads context internally)
- Wrap search cache-clear effect in requestAnimationFrame to match
favorites effect pattern in Conversations
- Use Set for O(1) lookups in MCPSelect server filtering
- Fix unnecessary JSX expression wrapper on string literal in
CreatePromptButton Link
* style(PromptTextCard): update icon classes for improved accessibility
- Add 'text-text-secondary' class to Check and Copy icons for better visibility and consistency in the PromptTextCard component.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* feat: Add GitNexus CI/CD and deployment configuration
- Introduced a Dockerfile for building the GitNexus application with necessary dependencies and configurations.
- Added a Caddyfile to set up a reverse proxy with bearer token authentication for secure access to GitNexus.
- Created an entrypoint script to validate the API token and start both GitNexus and Caddy services.
- Configured Fly.io deployment settings in fly.toml, including health checks and service parameters.
- Established GitHub Actions workflows for deploying the GitNexus index and managing deployments to Fly.io.
* fix: use npx instead of bunx for native addon compatibility
bunx skips node-gyp lifecycle scripts, so @ladybugdb/core's native
.node binary never gets compiled/downloaded. npx handles this correctly.
* fix: Use resolved provider for agent token lookup on custom endpoints
The providerEndpointMap lookup in initializeAgent used the original
provider name (e.g. "EduGPT") instead of the resolved overrideProvider
("openai"). Since providerEndpointMap only contains 4 built-in
providers, custom providers resolved to undefined, causing
getModelMaxTokens to miss the token map and fall back to 18000 tokens.
With agent instructions + tool schemas consuming most of that budget,
createPruneMessages would strip all messages on the first turn.
* fix: Use correct EndpointTokenConfig type in test
* refactor: Unify test factory, remove non-discriminating test
Address review findings:
- Remove Test 2 ("uses the model real context window") which passed
with and without the fix due to getModelMaxTokens defaulting to
openAI when endpoint is undefined (JS default parameter semantics)
- Merge createCustomProviderMocks into createMocks via provider,
overrideProvider, and useRealTokenLookup parameters
- Hoist jest.requireActual to file scope for shared access
* refactor: Address followup review findings
- Replace loose `maxContextTokens > 18000` assertion with precise
computed value `Math.round((65536 - 4096) * 0.95)` so the outcome
assertion is meaningful and self-documenting
- Hoist `customProvider` to describe-level constant `CUSTOM_PROVIDER`
- Document `overrideProvider` semantics and `useRealTokenLookup` in
factory JSDoc
- Add comment on real `optionalChainWithEmptyCheck` noting its
zero-handling semantics are load-bearing for the maxContextTokens=0
test
* style: Use // for inline comment, clarify pipeline assertion role
* fix: hide Delete Account button when ALLOW_ACCOUNT_DELETION is false
* fix: add admin bypass, inline env read, and tests for allowAccountDeletion
- Show delete button for admin users even when ALLOW_ACCOUNT_DELETION=false,
matching the canDeleteAccount middleware's ACCESS_ADMIN bypass
- Move env var read inline in buildSharedPayload() for per-request evaluation
- Add 4 frontend tests for Account conditional rendering
- Add 3 backend tests for allowAccountDeletion config field
* fix: use server-side ACCESS_ADMIN capability check instead of frontend role check
- Replace frontend SystemRoles.ADMIN check with server-side hasCapability()
in the authenticated config route, matching canDeleteAccount middleware exactly
- Admin bypass now evaluates ACCESS_ADMIN capability per-user in GET /api/config,
so users with the grant (regardless of role) see the button, and admins
without the grant do not
- Add 3 authenticated backend tests: without capability, with capability,
and skip-when-already-enabled
- Simplify frontend to pure config check (no role logic)
- Remove redundant jest-dom import; add inline env var comment
* test: add missing toHaveBeenCalled assertion in ACCESS_ADMIN test
* 📦 chore: npm audit fix
- Bump `vite` from 7.3.1 to 7.3.2.
- Upgrade `@chevrotain/cst-dts-gen`, `@chevrotain/gast`, `@chevrotain/regexp-to-ast`, `@chevrotain/types`, and `@chevrotain/utils` from 11.1.2 to 12.0.0.
- Update `@hono/node-server` from 1.19.10 to 1.19.13.
- Upgrade `chevrotain` from 11.1.2 to 12.0.0.
- Bump `chevrotain-allstar` from 0.3.1 to 0.4.1.
* 🔧 chore: Remove `serialize-javascript` dependency from `package.json`
* fix: reuse existing OAuth client registrations to prevent client_id mismatch
When using auto-discovered OAuth (DCR), LibreChat calls /register on every
flow initiation, getting a new client_id each time. When concurrent
connections or reconnections happen, the client_id used during /authorize
differs from the one used during /token, causing the server to reject the
exchange.
Before registering a new client, check if a valid client registration
already exists in the database and reuse it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Handle re-registration of OAuth clients when redirect_uri changes
* Add undefined fields for logo_uri and tos_uri in OAuth metadata tests
* test: add client registration reuse tests for horizontal scaling race condition
Reproduces the client_id mismatch bug that occurs in multi-replica deployments
where concurrent initiateOAuthFlow calls each register a new OAuth client.
Tests verify that the findToken-based client reuse prevents re-registration.
* fix: address review findings for client registration reuse
- Fix empty redirect_uris bug: invert condition so missing/empty
redirect_uris triggers re-registration instead of silent reuse
- Revert undocumented config?.redirect_uri in auto-discovery path
- Change DB error logging from debug to warn for operator visibility
- Fix import order: move package type import to correct section
- Remove redundant type cast and misleading JSDoc comment
- Test file: remove dead imports, restore process.env.DOMAIN_SERVER,
rename describe blocks, add empty redirect_uris edge case test,
add concurrent reconnection test with pre-seeded token,
scope documentation to reconnection stabilization
* fix: resolve type check errors for OAuthClientInformation redirect_uris
The SDK's OAuthClientInformation type lacks redirect_uris (only on
OAuthClientInformationFull). Cast to the local OAuthClientInformation
type in handler.ts when accessing deserialized client info from DB,
and use intersection types in tests for clientInfo with redirect_uris.
* fix: address follow-up review findings R1, R2, R3
- R1: Move `import type { TokenMethods }` to the type-imports section,
before local types, per CLAUDE.md import order rules
- R2: Add unit test for empty redirect_uris in handler.test.ts to
verify the inverted condition triggers re-registration
- R3: Use delete for process.env.DOMAIN_SERVER restoration when the
original value was undefined to avoid coercion to string "undefined"
* fix: clear stale client registration on OAuth flow failure
When a stored client_id is no longer recognized by the OAuth server,
the flow fails but the stale client stays in MongoDB, causing every
retry to reuse the same invalid registration in an infinite loop.
On OAuth failure, clear the stored client registration so the next
attempt falls through to fresh Dynamic Client Registration.
- Add MCPTokenStorage.deleteClientRegistration() for targeted cleanup
- Call it from MCPConnectionFactory's OAuth failure path
- Add integration test proving recovery from stale client reuse
* fix: validate auth server identity and target cleanup to reused clients
- Gate client reuse on authorization server identity: compare stored
issuer against freshly discovered metadata before reusing, preventing
wrong-client reuse when the MCP server switches auth providers
- Add reusedStoredClient flag to MCPOAuthFlowMetadata so cleanup only
runs when the failed flow actually reused a stored registration,
not on unrelated failures (timeouts, user-denied consent, etc.)
- Add cleanup in returnOnOAuth path: when a prior flow that reused a
stored client is detected as failed, clear the stale registration
before re-initiating
- Add tests for issuer mismatch and reusedStoredClient flag assertions
* fix: address minor review findings N3, N5, N6
- N3: Type deleteClientRegistration param as TokenMethods['deleteTokens']
instead of Promise<unknown>
- N5: Elevate deletion failure logging from debug to warn for operator
visibility when stale client cleanup fails
- N6: Use getLogPrefix() instead of hardcoded log prefix to respect
system-user privacy convention
* fix: correct stale-client cleanup in both OAuth paths
- Blocking path: remove result?.clientInfo guard that made cleanup
unreachable (handleOAuthRequired returns null on failure, so
result?.clientInfo was always false in the failure branch)
- returnOnOAuth path: only clear stored client when the prior flow
status is FAILED, not on COMPLETED or PENDING flows, to avoid
deleting valid registrations during normal flow replacement
* fix: remove redundant cast on clientMetadata
clientMetadata is already typed as Record<string, unknown>; the
as Record<string, unknown> cast was a no-op.
* fix: thread reusedStoredClient through return type instead of re-reading flow state
FlowStateManager.createFlow() deletes FAILED flow state before
rejecting, so getFlowState() after handleOAuthRequired() returns null
would find nothing — making the stale-client cleanup dead code.
Fix: hoist reusedStoredClient flag from flowMetadata into a local
variable, include it in handleOAuthRequired()'s return type (both
success and catch paths), and use result.reusedStoredClient directly
in the caller instead of a second getFlowState() round-trip.
* fix: selective stale-client cleanup in returnOnOAuth path
The returnOnOAuth cleanup was unreliable: it depended on reading
FAILED flow state, but FlowStateManager.monitorFlow() deletes FAILED
state before rejecting. Move cleanup into createFlow's catch handler
where flowMetadata.reusedStoredClient is still in scope.
Make cleanup selective in both paths: add isClientRejection() helper
that only matches errors indicating the OAuth server rejected the
client_id (invalid_client, unauthorized_client, client not found).
Timeouts, user-cancelled flows, and other transient failures no
longer wipe valid stored registrations.
Thread the error from handleOAuthRequired() through the return type
so the blocking path can also check isClientRejection().
* fix: tighten isClientRejection heuristic
Narrow 'client_id' match to 'client_id mismatch' to avoid
false-positive cleanup on unrelated errors that happen to
mention client_id.
* test: add isClientRejection tests and enforced client_id on test server
- Add isClientRejection unit tests: invalid_client, unauthorized_client,
client_id mismatch, client not found, unknown client, and negative
cases (timeout, flow state not found, user denied, null, undefined)
- Enhance OAuth test server with enforceClientId option: binds auth
codes to the client_id that initiated /authorize, rejects token
exchange with mismatched or unregistered client_id (401 invalid_client)
- Add integration tests proving the test server correctly rejects
stale client_ids and accepts matching ones at /token
* fix: issuer validation, callback error propagation, and cleanup DRY
- Issuer check: re-register when storedIssuer is absent or non-string
instead of silently reusing. Narrows unknown type with typeof guard
and inverts condition so missing issuer → fresh DCR (safer default).
- OAuth callback route: call failFlow with the OAuth error when the
authorization server redirects back with error= parameter, so the
waiting flow receives the actual rejection instead of timing out.
This lets isClientRejection match stale-client errors correctly.
- Extract duplicated cleanup block to clearStaleClientIfRejected()
private method, called from both returnOnOAuth and blocking paths.
- Test fixes: add issuer to stored metadata in reuse tests, reset
server to undefined in afterEach to prevent double-close.
* fix: gate failFlow behind callback validation, propagate reusedStoredClient on join
- OAuth callback: move failFlow call to after CSRF/session/active-flow
validation so an attacker with only a leaked state parameter cannot
force-fail a flow without passing the same integrity checks required
for legitimate callbacks
- PENDING join path: propagate reusedStoredClient from flow metadata
into the return object so joiners can trigger stale-client cleanup
if the joined flow later fails with a client rejection
* fix: restore early oauthError/code redirects, gate only failFlow behind CSRF
The previous restructuring moved oauthError and missing-code checks
behind CSRF validation, breaking tests that expect those redirects
without cookies. The redirect itself is harmless (just shows an error
page). Only the failFlow call needs CSRF gating to prevent DoS.
Restructure: oauthError check stays early (redirects immediately),
but failFlow inside it runs the full CSRF/session/active-flow
validation before marking the flow as FAILED.
* fix: require deleteTokens for client reuse, add missing import in MCP.js
Client registration reuse without cleanup capability creates a
permanent failure loop: if the reused client is stale, the code
detects the rejection but cannot clear the stored registration
because deleteTokens is missing, so every retry reuses the same
broken client_id.
- MCPConnectionFactory: only pass findToken to initiateOAuthFlow
when deleteTokens is also available, ensuring reuse is only
enabled when recovery is possible
- api/server/services/MCP.js: add deleteTokens to the tokenMethods
object (was the only MCP call site missing it)
* fix: set reusedStoredClient before createFlow in joined-flow path
When joining a PENDING flow, reusedStoredClient was only set on the
success return but not before the await. If createFlow throws (e.g.
invalid_client during token exchange), the outer catch returns the
local variable which was still false, skipping stale-client cleanup.
* fix: require browser binding (CSRF/session) for failFlow on OAuth error
hasActiveFlow only proves a PENDING flow exists, not that the caller
is the same browser that initiated it. An attacker with a leaked state
could force-fail the flow without any user binding. Require hasCsrf or
hasSession before calling failFlow on the oauthError path.
* fix: guard findToken with deleteTokens check in blocking OAuth path
Match the returnOnOAuth path's defense-in-depth: only enable client
registration reuse when deleteTokens is also available, ensuring
cleanup is possible if the reused client turns out to be stale.
* fix: address review findings — tests, types, normalization, docs
- Add deleteTokens method to InMemoryTokenStore matching TokenMethods
contract; update test call site from deleteToken to deleteTokens
- Add MCPConnectionFactory test: returnOnOAuth flow fails with
invalid_client → clearStaleClientIfRejected invoked automatically
- Add mcp.spec.js tests: OAuth error with CSRF → failFlow called;
OAuth error without cookies → failFlow NOT called (DoS prevention)
- Add JSDoc to isClientRejection with RFC 6749 and vendor attribution
- Add inline comment explaining findToken/deleteTokens coupling guard
- Normalize issuer comparison: strip trailing slashes to prevent
spurious re-registrations from URL formatting differences
- Fix dead-code: use local reusedStoredClient variable in PENDING
join return instead of re-reading flowMeta
* fix: address final review nits N1-N4
- N1: Add session cookie failFlow test — validates the hasSession
branch triggers failFlow on OAuth error callback
- N2: Replace setTimeout(50) with setImmediate for microtask drain
- N3: Add 'unknown client' attribution to isClientRejection JSDoc
- N4: Remove dead getFlowState mock from failFlow tests
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: pass explicit primaryKey to Meilisearch addDocuments/updateDocuments calls
Meilisearch v1.0+ refuses to auto-infer the primary key when a document
contains multiple fields ending with 'id'. The messages index has both
conversationId and messageId, causing addDocuments to silently fail with
index_primary_key_multiple_candidates_found, leaving message search empty.
Pass { primaryKey } to addDocumentsInBatches, addDocuments, and
updateDocuments — the variable was already in scope.
Also replace raw this.collection.updateMany with Mongoose Model.updateMany
to satisfy the no-restricted-syntax ESLint rule (tenant isolation guard).
Closes#12538
* fix: resolve additional Meilisearch plugin bugs found in review
Address review findings from PR #12542:
- Fix deleteObjectFromMeili using MongoDB _id instead of the Meilisearch
primary key (conversationId/messageId), causing post-remove cleanup to
silently no-op and leave orphaned documents in the index.
- Pass options.primaryKey explicitly to createMeiliMongooseModel factory
instead of deriving it from attributesToIndex[0] (schema field order),
eliminating a fragile implicit contract.
- Fix updateObjectToMeili skipping preprocessObjectForIndex, which meant
updates bypassed content array-to-text conversion and conversationId
pipe character escaping.
- Change collection.updateMany to collection.updateOne in addObjectToMeili
since _id is unique (semantic correctness).
- Add primaryKey to validateOptions required keys.
- Strengthen test assertions to verify { primaryKey } argument is passed
to addDocuments, addDocumentsInBatches, and updateDocuments. Add tests
for the update path including preprocessObjectForIndex pipe escaping.
* fix: add regression tests for delete and message update paths
Address follow-up review findings:
- Add test for deleteObjectFromMeili verifying it uses messageId (not
MongoDB _id) when calling index.deleteDocument, guarding against
regression of the silent orphaned-document bug.
- Add test for message model update path asserting { primaryKey:
'messageId' } is passed to updateDocuments (previously only the
conversation model update path was tested).
- Add @param config.primaryKey to createMeiliMongooseModel JSDoc.
* 🔧 chore: Update `mongodb-memory-server` to v11.0.1
- Bump `mongodb-memory-server` version in `package-lock.json`, `api/package.json`, and `packages/data-schemas/package.json` from 10.1.4 to 11.0.1.
- Update related dependencies in `mongodb-memory-server` and `mongodb-memory-server-core` to ensure compatibility with the new version.
- Adjust `tslib` version in `mongodb-memory-server` to 2.8.1 and `debug` to 4.4.3 for consistency.
* chore: npm audit fix
* chore: Update `mermaid` dependency to version 11.14.0 in `package-lock.json` and `client/package.json`
* fix: use deterministic timestamps in convoStructure test
MongoDB 8.x (from mongodb-memory-server v11) no longer guarantees
insertion-order return for documents with identical timestamps.
Use sequential timestamps with overrideTimestamp to ensure buildTree
processes parents before children.
* directly returns the translation function without managing language state in client package
* chore: remove unused langAtom from packages/client store
* fix: add useCallback to match canonical useLocalize, add guard comment
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
Right now, if you have draft text in conversation A, but no draft text in
conversation B, then switching from A -> B inserts the draft from A into B
(oops).
This was caused by a bug in the `restoreText()` logic which did not
restore *blank* text as the saved draft.
Now, it'll always restore whatever is found as a draft (or set to blank if
there is no draft).
* fix: Resolve custom role permissions not loading in frontend
Users assigned to custom roles (non-USER/ADMIN) had all permission
checks fail because AuthContext only fetched system role permissions.
The roles map keyed by USER/ADMIN never contained the custom role name,
so useHasAccess returned false for every feature gate.
- Fetch the user's custom role in AuthContext and include it in the
roles map so useHasAccess can resolve permissions correctly
- Use encodeURIComponent instead of toLowerCase for role name URLs
to preserve custom role casing through the API roundtrip
- Only uppercase system role names on the backend GET route; pass
custom role names through as-is for exact DB lookup
- Allow users to fetch their own assigned role without READ_ROLES
capability
* refactor: Normalize all role names to uppercase
Custom role names were stored in original casing, causing case-sensitivity
bugs across the stack — URL lowercasing, route uppercasing, and
case-sensitive DB lookups all conflicted for mixed-case custom roles.
Enforce uppercase normalization at every boundary:
- createRoleByName trims and uppercases the name before storage
- createRoleHandler uppercases before passing to createRoleByName
- All admin route handlers (get, update, delete, members, permissions)
uppercase the :name URL param before DB lookups
- addRoleMemberHandler uppercases before setting user.role
- Startup migration (normalizeRoleNames) finds non-uppercase custom
roles, renames them, and updates affected user.role values with
collision detection
Legacy GET /api/roles/:roleName retains always-uppercase behavior.
Tests updated to expect uppercase role names throughout.
* fix: Use case-preserved role names with strict equality
Remove uppercase normalization — custom role names are stored and
compared exactly as the user sets them, with only trimming applied.
USER and ADMIN remain reserved case-insensitively via isSystemRoleName.
- Remove toUpperCase from createRoleByName, createRoleHandler, and
all admin route handlers (get, update, delete, members, permissions)
- Remove toUpperCase from legacy GET and PUT routes in roles.js;
the frontend now sends exact casing via encodeURIComponent
- Remove normalizeRoleNames startup migration
- Revert test expectations to original casing
* fix: Format useMemo dependency array for Prettier
* feat: Add custom role support to admin settings + review fixes
- Add backend tests for isOwnRole authorization gate on GET /api/roles/:roleName
- Add frontend tests for custom role detection and fetching in AuthContext
- Fix transient null permission flash by only spreading custom role once loaded
- Add isSystemRoleName helper to data-provider for case-insensitive system role detection
- Use sentinel value in useGetRole to avoid ghost cache entry from empty string
- Add useListRoles hook and listRoles data service for fetching all roles
- Update AdminSettingsDialog and PeoplePickerAdminSettings to dynamically
list custom roles in the role dropdown, with proper fallback defaults
* fix: Address review findings for custom role permissions
- Add assertions to AuthContext test verifying custom role in roles map
- Fix empty array bypassing nullish coalescing fallback in role dropdowns
- Add null/undefined guard to isSystemRoleName helper
- Memoize role dropdown items to avoid unnecessary re-renders
- Apply sentinel pattern to useGetRole in admin settings for consistency
- Mark ListRolesResponse description as required to match schema
* fix: Prevent prototype pollution in role authorization gate
- Replace roleDefaults[roleName] with Object.hasOwn to prevent
prototype chain bypass for names like constructor or __proto__
- Add dedicated rolesList query key to avoid cache collision when
a custom role is named 'list'
- Add regression test for prototype property name authorization
* fix: Resolve Prettier formatting and unused variable lint errors
* fix: Address review findings for custom role permissions
- Add ADMIN self-read test documenting isOwnRole bypass behavior
- Guard save button while custom role data loads to prevent data loss
- Extract useRoleSelector hook eliminating ~55 lines of duplication
- Unify defaultValues/useEffect permission resolution (fixes inconsistency)
- Make ListRolesResponse.description and _id optional to match schema
- Fix vacuous test assertions to verify sentinel calls exist
- Only fetch userRole when user.role === USER (avoid unnecessary requests)
- Remove redundant empty string guard in custom role detection
* fix: Revert USER role fetch restriction to preserve admin settings
Admins need the USER role loaded in AuthContext.roles so the admin
settings dialog shows persisted USER permissions instead of defaults.
* fix: Remove unused useEffect import from useRoleSelector
* fix: Clean up useRoleSelector hook
- Use existing isCustom variable instead of re-calling isSystemRoleName
- Remove unused roles and availableRoleNames from return object
* fix: Address review findings for custom role permissions
- Use Set-based isSystemRoleName to auto-expand with future SystemRoles
- Add isCustomRoleError handling: guard useEffect reset and disable Save
- Remove resolvePermissions from hook return; use defaultValues in useEffect
to eliminate redundant computation and stale-closure reset race
- Rename customRoleName to userRoleName in AuthContext for clarity
* fix: Request server-max roles for admin dropdown
listRoles now passes limit=200 (the server's MAX_PAGE_LIMIT) so the
admin role selector shows all roles instead of silently truncating
at the default page size of 50.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* chore: remove unused `interface.endpointsMenu` config field
* chore: address review — restore JSDoc UI-only example, add Zod strip test
* chore: remove unused `interface.sidePanel` config field
* chore: restrict fileStrategy/fileStrategies schema to valid storage backends
* fix: use valid FileStorage value in AppService test
* chore: address review — version bump, exhaustiveness guard, JSDoc, configSchema test
* chore: remove debug logger.log from MessageIcon render path
* fix: rewrite MessageIcon render tests to use render counting instead of logger spying
* chore: bump librechat-data-provider to 0.8.407
* chore: sync example YAML version to 1.3.7
* debug: add instrumentation to MessageIcon arePropsEqual + render cycle tests
Temporary debug commit to identify which field triggers MessageIcon
re-renders during message creation and streaming.
arePropsEqual now logs 'icon_memo_diff' with the exact field name and
prev/next values whenever it returns false. Filter browser console for
'icon_memo_diff' to see the trigger.
Also adds render-level integration tests that simulate the message
lifecycle (initial mount, streaming chunks, context updates) and
assert render counts via logger spy.
* perf: stabilize MultiMessage key to prevent unmount/remount during SSE lifecycle
messageId changes 3 times during the SSE message lifecycle:
1. useChatFunctions creates initialResponse with client-generated UUID
2. createdHandler replaces it with userMessageId + '_'
3. finalHandler replaces it with server-assigned messageId
Since MultiMessage used key={message.messageId}, each change caused
React to destroy and recreate the entire message component subtree,
unmounting MessageIcon and all memoized children. This produced visible
icon/image flickering that no memo comparator could prevent.
Switch to key={parentMessageId + '_' + siblingIdx}:
- parentMessageId is stable from creation through final response
- siblingIdx ensures sibling switches still get clean remounts
- Eliminates 2 unnecessary unmount/remount cycles per message
Add key stability tests verifying:
- Current key={messageId} causes 3 mounts / 2 unmounts per lifecycle
- Stable key causes 1 mount / 0 unmounts per lifecycle
- Sibling switches still trigger clean remounts with stable key
* perf: stabilize root MultiMessage key across new conversation lifecycle
When a user sends their first message in a new conversation,
conversationId transitions from null/'new' to the server-assigned
UUID. MessagesView used key={conversationId} on the root MultiMessage,
so this transition destroyed the entire message tree and rebuilt it
from scratch — causing all MessageIcons to unmount/remount (visible
as image flickering).
Use a ref-based stable key that captures the first real conversationId
and only changes on genuine conversation switches (navigating to a
different conversation), not on the null→UUID transition within the
same conversation.
* debug: add mount/unmount lifecycle tracking to MessageIcon
Adds icon_lifecycle logs (MOUNT/UNMOUNT) and render count to
distinguish between fresh mounts (memo comparator not called)
and internal re-renders (hook bypassing memo).
Enable: localStorage.setItem('DEBUG_LOGGING', 'icon_lifecycle,icon_data,icon_memo_diff')
* debug: add key and root tracking to MultiMessage and MessagesView
Logs multi_message_key (stableKey, messageId, parentMessageId, route)
and messages_view_key (rootKey, conversationId) to trace which key
changes trigger unmount/remount cycles.
Enable: localStorage.setItem('DEBUG_LOGGING', 'icon_lifecycle,icon_data,icon_memo_diff,multi_message_key,messages_view_key')
* perf: remove key from root MultiMessage to prevent tree destruction
The ref-based stable key still changed during 'new' → real UUID
transition, destroying the entire tree. The root MultiMessage is the
sole child at its position, so React reuses the instance via
positional reconciliation without any key. The messageId prop
(conversationId) naturally resets Recoil siblingIdxFamily state on
conversation switches.
* perf: remove unstable keys from MultiMessage to prevent SSE lifecycle remounts
Both messageId and parentMessageId change during the SSE lifecycle
(client UUID → CREATED server ID → FINAL server ID), making neither
viable as a stable React key. Each key change caused React to destroy
and recreate the entire message component subtree, including all
memoized children — visible as icon/image flickering.
Remove explicit keys entirely and rely on React's positional
reconciliation. MultiMessage always renders exactly one child at
the same position, so React reuses the component instance and
updates props in place. The existing memo comparators on
ContentRender/MessageRender handle field-level diffing correctly.
Update tests to verify: key={messageId} causes 3 mounts/2 unmounts
per lifecycle, while no key causes 1 mount/0 unmounts.
* perf: remove unstable keys from child MultiMessage in message wrappers
Message.tsx, MessageContent.tsx, and MessageParts.tsx each render a
child MultiMessage with key={messageId} for the current message's
children. Since messageId changes during the SSE lifecycle (CREATED
event replaces the user message ID), the child MultiMessage gets
destroyed and recreated, unmounting the entire agent response subtree
including its MessageIcon.
Remove these keys for the same reason as the parent MultiMessage:
each child MultiMessage renders exactly one child at a fixed position,
so positional reconciliation correctly reuses the instance.
* chore: remove MultiMessage key tests — they test React behavior, not our code
The tests verified that key={messageId} causes remounts while no key
doesn't, but this is React's own reconciliation behavior. No unit test
can prevent someone from re-adding a key prop to JSX. The JSDoc comments
on MultiMessage document the decision and rationale.
* 🔐 fix: Strip code_challenge from admin OAuth requests before Passport
openid-client v6's Passport Strategy uses `currentUrl.searchParams.size === 0`
to distinguish initial authorization requests from OAuth callbacks. The
admin-panel-specific `code_challenge` query parameter caused the strategy to
misclassify the request as a callback and return 401 Unauthorized.
* 🔐 fix: Strip code_challenge from admin OAuth requests before Passport
openid-client v6's Passport Strategy uses `currentUrl.searchParams.size === 0`
to distinguish initial authorization requests from OAuth callbacks. The
admin-panel-specific `code_challenge` query parameter caused the strategy to
misclassify the request as a callback and return 401 Unauthorized.
- Fix regex to handle `code_challenge` in any query position without producing
malformed URLs, and handle empty `code_challenge=` values (`[^&]*` vs `[^&]+`)
- Combine `storePkceChallenge` + `stripCodeChallenge` into a single
`storeAndStripChallenge` helper to enforce read-store-strip ordering
- Apply defensively to all 7 admin OAuth providers
- Add 12 unit tests covering stripCodeChallenge and storeAndStripChallenge
* refactor: Extract PKCE helpers to utility file, harden tests
- Move stripCodeChallenge and storeAndStripChallenge to
api/server/utils/adminPkce.js — eliminates _test production export
and avoids loading the full auth.js module tree in tests
- Add missing req.originalUrl/req.url assertions to invalid-challenge
and no-challenge test branches (regression blind spots)
- Hoist cache reference to module scope in tests (was redundantly
re-acquired from mock factory on every beforeEach)
* chore: Address review NITs — imports, exports, naming, assertions
- Fix import order in auth.js (longest-to-shortest per CLAUDE.md)
- Remove unused PKCE_CHALLENGE_TTL/PKCE_CHALLENGE_PATTERN exports
- Hoist strip arrow to module-scope stripChallengeFromUrl
- Rename auth.test.js → auth.spec.js (project convention)
- Tighten cache-failure test: toBe instead of toContain, add req.url
* refactor: Move PKCE helpers to packages/api with dependency injection
Move stripCodeChallenge and storeAndStripChallenge from api/server/utils
into packages/api/src/auth/exchange.ts alongside the existing PKCE
verification logic. Cache is now injected as a Keyv parameter, matching
the dependency-injection pattern used throughout packages/api/.
- Add PkceStrippableRequest interface for minimal req typing
- auth.js imports storeAndStripChallenge from @librechat/api
- Delete api/server/utils/adminPkce.js
- Move tests to packages/api/src/auth/adminPkce.spec.ts (TypeScript,
real Keyv instances, no getLogStores mock needed)
* fix: check supportedMimeTypes before routing unrecognized file types
In processAttachments, files not matching the hardcoded mime type
categories (image, PDF, video, audio) were silently dropped. Now
resolves the endpoint's file config and checks the file type against
supportedMimeTypes before routing to the documents pipeline. Files
not matching any config are still skipped (original behavior).
Closes#12482
* feat: encode generic document types for supported providers
Remove restrictive mime type filter in encodeAndFormatDocuments that
only allowed PDFs and application/* types. Add a generic encoding
path for non-PDF, non-Bedrock files using the provider's native
format (Anthropic base64 document, OpenAI file block, Google media
block). Files are already validated upstream by supportedMimeTypes.
* fix: guard file.type and cache file config in processAttachments
- Add file.type truthiness check before checkType to prevent
coercion of null/undefined to string 'null'/'undefined'
- Cache mergedFileConfig and endpointFileConfig on the instance
so addPreviousAttachments doesn't recompute per message
* refactor: harden generic document encoding with validation and tests
- Extract formatDocumentBlock helper to eliminate ~30 lines of
duplicate provider-dispatch code between PDF and generic paths
- Add size validation in generic encoding path using
configuredFileSizeLimit (was fetched but unused)
- Guard Bedrock from generic path — non-bedrockDocumentFormats
types are now skipped instead of silently tracking metadata
- Only push metadata to result.files when a document block was
actually created, preventing silent inconsistent state
- Enable Anthropic citations for text/plain, text/html,
text/markdown (supported by Anthropic's document API)
- Fix != to !== for Providers.AZURE comparison
- Add 9 tests covering all four provider branches, Bedrock
exclusion, size limit enforcement, and unhandled provider
* fix: resolve filename type mismatch in formatDocumentBlock
filename parameter is string | undefined but OpenAIFileBlock and
OpenAIInputFileBlock require string. Default to 'document' when
filename is undefined.
* fix: use endpoint name for file config lookup in processAttachments
Agent runs can have agent.provider set to a base provider (e.g.,
openAI) while agent.endpoint is a custom endpoint name. Using
provider for the getEndpointFileConfig lookup bypassed custom
endpoint supportedMimeTypes config. Now uses agent.endpoint,
matching the pattern in addDocuments.
* perf: filter non-Bedrock files before fetching streams
Bedrock only supports types in bedrockDocumentFormats. Previously,
getFileStream was called for all files and unsupported types were
discarded after download. Now pre-filters the file list for Bedrock
to avoid unnecessary network and memory overhead for large
unsupported attachments.
* refactor: clean up processAttachments file config handling
- Remove redundant ?? null intermediaries; use instance properties
directly in the else-if condition
- Add JSDoc @type annotations for _mergedFileConfig and
_endpointFileConfig in the constructor
* refactor: harden document encoding and add routing tests
- Hoist configuredFileSizeLimit above the loop to avoid recomputing
mergeFileConfig per file
- Replace Buffer.from decode with base64 length formula in the
generic size check to avoid unnecessary heap allocation
- Use nullish coalescing (??) for filename fallback
- Clean up test: remove unnecessary type cast, use createMockRequest
helper for size-limit test
- Add 14 tests for processAttachments categorization logic covering
supportedMimeTypes routing, null/undefined guards, standard type
passthrough, and edge cases
* fix: use optional chaining for checkType in routing tests
FileConfig.checkType is typed as optional. Use optional chaining
to satisfy strict type checking.
* fix: skip stream fetches for unsupported providers, block Bedrock generic routing
- Return early from encodeAndFormatDocuments when the provider is
neither document-supported nor Bedrock, avoiding unnecessary
getFileStream calls for providers that would discard all results
- Add !isBedrock guard to the supportedMimeTypes fallback branch in
processAttachments so permissive patterns like '.*' don't route
non-Bedrock types into documents that would be silently dropped
- Add test for Bedrock + non-Bedrock-document-type skipping
* fix: respect supportedMimeTypes config for Bedrock endpoints
Remove !isBedrock guard from the generic supportedMimeTypes routing
branch. If a user configures permissive supportedMimeTypes for a
Bedrock endpoint, the upload validation already accepted the file.
The encoding layer pre-filters to Bedrock-supported types before
fetching streams, so unsupported types are handled there without
silently dropping files the user explicitly allowed.
* fix: prevent MCP tools with `_action` in name from being misclassified as OpenAPI action tools
Add `isActionTool()` helper that checks for the `_action_` delimiter
while guarding against cross-delimiter collision with `_mcp_`. Replace
all `includes(actionDelimiter)` classification checks with the new
helper across backend and frontend.
* test: add coverage for MCP/action cross-delimiter collision
Verify that `isActionTool` correctly rejects MCP tool names containing
`_action` and that `loadAgentTools` does not filter them based on
`actionsEnabled`. Add ToolIcon and definitions test cases.
* fix: simplify isActionTool to handle all MCP name patterns
- Use `!toolName.includes('_mcp_')` instead of checking only after the
first `_action_` occurrence, which missed MCP tools with `_action_` in
the middle of their name (e.g. `get_action_data_mcp_myserver`).
- Reference `Constants.mcp_delimiter` value via a local const to avoid
circular import from config.ts, with a comment explaining why.
- Remove dead `actionDelimiter` import from definitions.ts.
- Replace double-filter with single-pass partition in loadToolsForExecution.
- Add test for mid-name `_action_` collision case.
* fix: narrow MCP exclusion to delimiter position in isActionTool
Only reject when `_mcp_` appears after `_action_` (the MCP suffix
position). `_mcp_` before `_action_` is part of the operationId and
is valid — e.g. `sync_mcp_state_action_api---example---com` is a
legitimate action tool whose operationId happens to contain `_mcp_`.
* fix: document positional _mcp_ guard and known RFC-invalid domain limitation
Expand JSDoc on isActionTool to explain the action/MCP format
disambiguation and the theoretical false negative for non-RFC-compliant
domains containing `_mcp_`. Add test documenting this known edge case.
* fix: omit externalResources for static Sandpack previews
The Tailwind CDN URL lacks a file extension, causing Sandpack's
static template to throw a runtime injection error. Static previews
already load Tailwind via a script tag in the shared index.html,
so externalResources is unnecessary for them.
Closes#12507
* refactor: extract buildSandpackOptions and add tests
- Surgically omit only externalResources for static templates
instead of discarding all sharedOptions, preventing future
regression if new template-agnostic options are added.
- Extract options logic into a pure, testable helper function.
- Add unit tests covering all template/config combinations.
* chore: fix import order and pin test assertions
* fix: use URL fragment hint instead of omitting externalResources
Sandpack's static template regex detects resource type from the URL's
last file extension. The versioned CDN path (/3.4.17) matched ".17"
instead of ".js", throwing "Unable to determine file type". Rather
than omitting externalResources for static templates (which would
remove the only Tailwind injection path for HTML artifacts that don't
embed their own script tag), append a #tailwind.js fragment hint so
the regex matches ".js". Fragments are not sent to the server, so
the CDN response is unchanged.
* fix: pass recursionLimit to processStream in OpenAI-compatible agents API
The OpenAI-compatible endpoint never passed recursionLimit to LangGraph's
processStream(), silently capping all API-based agent calls at the default
25 steps. Mirror the 3-step cascade already used by the UI path (client.js):
yaml config default → per-agent DB override → max cap.
* refactor: extract resolveRecursionLimit into shared utility
Extract the 3-step recursion limit cascade into a shared
resolveRecursionLimit() function in @librechat/api. Both openai.js and
client.js now call this single source of truth.
Also fixes falsy-guard edge cases where recursion_limit=0 or
maxRecursionLimit=0 would silently misbehave, by using explicit
typeof + positive checks.
Includes unit tests covering all cascade branches and edge cases.
* refactor: use resolveRecursionLimit in openai.js and client.js
Replace duplicated cascade logic in both controllers with the shared
resolveRecursionLimit() utility from @librechat/api.
In openai.js: hoist agentsEConfig to avoid double property walk,
remove displaced comment, add integration test assertions.
In client.js: remove inline cascade that was overriding config
after initial assignment.
* fix: hoist processStream mock for test accessibility
The processStream mock was created inline inside mockResolvedValue,
making it inaccessible via createRun.mock.results (which returns
the Promise, not the resolved value). Hoist it to a module-level
variable so tests can assert on it directly.
* test: improve test isolation and boundary coverage
Use mockReturnValueOnce instead of mockReturnValue to prevent mock
leaking across test boundaries. Add boundary tests for downward
agent override and exact-match maxRecursionLimit.
* refactor: self-healing tenant isolation update guard
Replace the strict throw-on-any-tenantId guard with a
strip-or-throw approach:
- $set/$setOnInsert: strip when value matches current tenant
or no context is active; throw only on cross-tenant mutations
- $unset/$rename: always strip (unsetting/renaming tenantId
is never valid)
- Top-level tenantId: same logic as $set
This eliminates the entire class of "tenantId in update payload"
bugs at the plugin level while preserving the cross-tenant
security invariant.
* test: update mutation guard tests for self-healing behavior
- Convert same-tenant $set/$setOnInsert tests to expect silent
stripping instead of throws
- Convert $unset test to expect silent stripping
- Add cross-tenant throw tests for $set, $setOnInsert, top-level
- Add same-tenant stripping tests for $set, $setOnInsert, top-level
- Add $rename stripping test
- Add no-context stripping test
- Update error message assertions to match new cross-tenant message
* revert: remove call-site tenantId stripping patches
Revert the per-call-site tenantId stripping from #12498 and
the excludedKeys patch from #12501. These are no longer needed
since the self-healing guard handles tenantId in update payloads
at the plugin level.
Reverted patches:
- conversation.ts: delete update.tenantId in saveConvo(),
tenantId destructuring in bulkSaveConvos()
- message.ts: delete update.tenantId in saveMessage() and
recordMessage(), tenantId destructuring in bulkSaveMessages()
and updateMessage()
- config.ts: tenantId in excludedKeys Set
- config.spec.ts: tenantId in excludedKeys test assertion
* fix: strip tenantId from update documents in tenantSafeBulkWrite
Mongoose middleware does not fire for bulkWrite, so the plugin-level
guard never sees update payloads in bulk operations. Extend
injectTenantId() to strip tenantId from update documents for
updateOne/updateMany operations, preventing cross-tenant overwrites.
* refactor: rename guard, add empty-op cleanup and strict-mode warning
- Rename assertNoTenantIdMutation to sanitizeTenantIdMutation
- Remove empty operator objects after stripping to avoid MongoDB errors
- Log warning in strict mode when stripping tenantId without context
- Fix $setOnInsert test to use upsert:true with non-matching filter
* test: fix bulk-save tests and add negative excludedKeys assertion
- Wrap bulkSaveConvos/bulkSaveMessages tests in tenantStorage.run()
to exercise the actual multi-tenant stripping path
- Assert tenantId equals the real tenant, not undefined
- Add negative assertion: excludedKeys must NOT contain tenantId
* fix: type-safe tenantId stripping in tenantSafeBulkWrite
- Fix TS2345 error: replace conditional type inference with
UpdateQuery<Record<string, unknown>> for stripTenantIdFromUpdate
- Handle empty updates after stripping (e.g., $set: { tenantId } as
sole field) by filtering null ops from the bulk array
- Add 4 tests for bulk update tenantId stripping: plain-object update,
$set stripping, $unset stripping, and sole-field-in-$set edge case
* fix: resolve TS2345 in stripTenantIdFromUpdate parameter type
Use Record<string, unknown> instead of UpdateQuery<> to avoid
type incompatibility with Mongoose's AnyObject-based UpdateQuery
resolution in CI.
* fix: strip tenantId from bulk updates unconditionally
Separate sanitization from injection in tenantSafeBulkWrite:
tenantId is now stripped from all update documents before any
tenant-context checks, closing the gap where no-context and
system-context paths passed caller-supplied tenantId through
to MongoDB unmodified.
* refactor: address review findings in tenant isolation
- Fix early-return gap in stripTenantIdFromUpdate that skipped
operator-level tenantId when top-level was also present
- Lazy-allocate copy in stripTenantIdFromUpdate (no allocation
when no tenantId is present)
- Document behavioral asymmetry: plugin throws on cross-tenant,
bulkWrite strips silently (intentional, documented in JSDoc)
- Remove double JSDoc on injectTenantId
- Remove redundant cast in stripTenantIdFromUpdate
- Use shared frozen EMPTY_BULK_RESULT constant
- Remove Record<string, unknown> annotation in recordMessage
- Isolate bulkSave* tests: pre-create docs then update with
cross-tenant payload, read via runAsSystem to prove stripping
is independent of filter injection
* fix: no-op empty updates after tenantId sanitization
When tenantId is the sole field in an update (e.g., { $set: { tenantId } }),
sanitization leaves an empty update object that would fail with
"Update document requires atomic operators." The updateGuard now
detects this and short-circuits the query by adding an unmatchable
filter condition and disabling upsert, matching the bulk-write
handling that filters out null ops.
* refactor: remove dead logger.warn branches, add mixed-case test
- Remove unreachable logger.warn calls in sanitizeTenantIdMutation:
queryMiddleware throws before updateGuard in strict+no-context,
and isStrict() is false in non-strict+no-context
- Add test for combined top-level + operator-level tenantId stripping
to lock in the early-return fix
* feat: ESLint rule to ban raw bulkWrite and collection.* in data-schemas
Add no-restricted-syntax rules to the data-schemas ESLint config that
flag direct Model.bulkWrite() and Model.collection.* calls. These
bypass Mongoose middleware and the tenant isolation plugin — all bulk
writes must use tenantSafeBulkWrite() instead.
Test files are excluded since they intentionally use raw driver calls
for fixture setup.
Also migrate the one remaining raw bulkWrite in seedSystemGrants() to
use tenantSafeBulkWrite() for consistency.
* test: add findByIdAndUpdate coverage to mutation guard tests
* fix: keep tenantSafeBulkWrite in seedSystemGrants, fix ESLint config
- Revert to tenantSafeBulkWrite in seedSystemGrants (always runs
under runAsSystem, so the wrapper passes through correctly)
- Split data-schemas ESLint config: shared TS rules for all files,
no-restricted-syntax only for production non-wrapper files
- Fix unused destructure vars to use _tenantId pattern
* fix: auth-aware config caching for fresh sessions
- Add auth state to startup config query key via shared `startupConfigKey`
builder so login (unauthenticated) and chat (authenticated) configs are
cached independently
- Disable queries during login onMutate to prevent premature unauthenticated
refetches after cache clear
- Re-enable queries in setUserContext only after setTokenHeader runs, with
positive-only guard to avoid redundant disable on logout
- Update all getQueryData call sites to use the shared key builder
- Fall back to getConfigDefaults().interface in useEndpoints, hoisted to
module-level constant to avoid per-render recomputation
* fix: address review findings for auth-aware config caching
- Move defaultInterface const after all imports in ModelSelector.tsx
- Remove dead QueryKeys import, use import type for TStartupConfig
in ImportConversations.tsx
- Spread real exports in useQueryParams.spec.ts mock to preserve
startupConfigKey, fixing TypeError in all 6 tests
* chore: import order
* fix: re-enable queries on login failure
When login fails, onSuccess never fires so queriesEnabled stays
false. Re-enable in onError so the login page can re-fetch config
(needed for LDAP username validation and social login options).
`BaseClient.js` iterates existing conversation keys to build `unsetFields`
for removal when `endpointOptions` doesn't include them. When tenant
isolation stamps `tenantId` on the document, it gets swept into `$unset`,
triggering `assertNoTenantIdMutation`. Adding `tenantId` to `excludedKeys`
prevents this — it's a system field, not an endpoint option.
* fix: Exclude field from conversation and message updates
* fix: Remove tenantId from conversation and message update objects to prevent unintended data exposure.
* refactor: Adjust update logic in createConversationMethods and createMessageMethods to ensure tenantId is not included in the updates, maintaining data integrity.
* fix: Strip tenantId from all write paths in conversation and message methods
Extends the existing tenantId stripping to bulkSaveConvos, bulkSaveMessages,
recordMessage, and updateMessage — all of which previously passed caller-supplied
tenantId straight through to the update document. Renames discard alias from _t
to _tenantId for clarity. Adds regression tests for all six write paths.
* fix: Eliminate double-copy overhead and strengthen test assertions
Replace destructure-then-spread with spread-once-then-delete for saveConvo,
saveMessage, and recordMessage — removes one O(n) copy per call on hot paths.
Add missing not-null and positive data assertions to tenantId stripping tests.
* test: Add positive data assertions to bulkSaveMessages and recordMessage tests
* fix: Allow empty-overrides scope creation when priority is provided
The upsertConfigOverrides handler short-circuited when overrides was
empty, returning a plain message instead of creating the config document.
This broke the admin panel's "create blank scope" flow which sends
`{ overrides: {}, priority: N }` — the missing `config` property in the
response caused an `_id` error on the client.
The early return now only triggers when both overrides are empty and no
priority is provided. Per-section permission checks are scoped to cases
where override sections are actually present.
* test: Add tests for empty-overrides scope creation with priority
* test: Address review nits for empty-overrides scope tests
- Add res.statusCode/res.body assertions to capability-check test
- Add 403/401 tests for empty overrides + priority path
- Use mockResolvedValue(null) for consistency on bare jest.fn()
- Remove narrating comment; fold intent into test name
* 🛡️ fix: restrict system grants to role principals only
Narrows GrantPrincipalType to PrincipalType.ROLE, rejecting GROUP and
USER with 400. Removes grant cascade cleanup from group/user deletion
handlers and their route wiring since only roles can hold grants.
* 🛡️ fix: address review findings for grants roles-only restriction
Add missing GROUP rejection test for revokeGrant (symmetric with
getPrincipalGrants and assignGrant coverage), add extensibility comment
to GrantPrincipalType, and document the checkRoleExists guard.
* refactor: split /api/config into unauthenticated and authenticated response paths
- Replace preAuthTenantMiddleware with optionalJwtAuth on the /api/config
route so the handler can detect whether the request is authenticated
- When unauthenticated: call getAppConfig({ baseOnly: true }) for zero DB
queries, return only login-relevant fields (social logins, turnstile,
privacy policy / terms of service from interface config)
- When authenticated: call getAppConfig({ role, userId, tenantId }) to
resolve per-user DB overrides (USER + ROLE + GROUP + PUBLIC principals),
return full payload including modelSpecs, balance, webSearch, etc.
- Extract buildSharedPayload() and addWebSearchConfig() helpers to avoid
duplication between the two code paths
- Fixes per-user balance overrides not appearing in the frontend because
userId was never passed to getAppConfig (follow-up to #12474)
* test: rewrite config route tests for unauthenticated vs authenticated paths
- Replace the previously-skipped supertest tests with proper mocked tests
- Cover unauthenticated path: baseOnly config call, minimal payload,
interface subset (privacyPolicy/termsOfService only), exclusion of
authenticated-only fields
- Cover authenticated path: getAppConfig called with userId, full payload
including modelSpecs/balance/webSearch, per-user balance override merging
* fix: address review findings — restore multi-tenant support, improve tests
- Chain preAuthTenantMiddleware back before optionalJwtAuth on /api/config
so unauthenticated requests in multi-tenant deployments still get
tenant-scoped config via X-Tenant-Id header (Finding #1)
- Use getAppConfig({ tenantId }) instead of getAppConfig({ baseOnly: true })
when a tenant context is present; fall back to baseOnly for single-tenant
- Fix @type annotation: unauthenticated payload is Partial<TStartupConfig>
- Refactor addWebSearchConfig into pure buildWebSearchConfig that returns a
value instead of mutating the payload argument
- Hoist isBirthday() to module level
- Remove inline narration comments
- Assert tenantId propagation in tests, including getTenantId fallback and
user.tenantId preference
- Add error-path tests for both unauthenticated and authenticated branches
- Expand afterEach env var cleanup for proper test isolation
* test: fix mock isolation and add tenant-scoped response test
- Replace jest.clearAllMocks() with jest.resetAllMocks() so
mockReturnValue implementations don't leak between tests
- Add test verifying tenant-scoped socialLogins and turnstile are
correctly mapped in the unauthenticated response
* fix: add optionalJwtAuth to /api/config in experimental.js
Without this middleware, req.user is never populated in the experimental
cluster entrypoint, so authenticated users always receive the minimal
unauthenticated config payload.
* perf: add custom memo comparator to MessageIcon for stable re-render gating
MessageIcon receives full `agent` and `assistant` objects as props from
useMessageActions, which recomputes them when AgentsMapContext or
AssistantsMapContext update (e.g., react-query refetch on window focus).
These context-triggered re-renders bypass MessageRender's React.memo,
producing new object references for agent/assistant even when the
underlying data is unchanged. The default shallow comparison in
MessageIcon's memo then fails, causing unnecessary re-renders that
manifest as visible icon flickering.
Add arePropsEqual comparator that checks only the fields MessageIcon
actually uses (name, avatar filepath, metadata avatar) instead of
object identity, so the component correctly bails out when icon-relevant
data hasn't changed.
* refactor: export arePropsEqual, drop redundant useMemos, add JSDoc
- Export arePropsEqual so it can be tested in isolation
- Add JSDoc documenting which fields are intentionally omitted (id)
and why iconData uses reference equality
- Replace five trivial useMemo calls (agent?.name ?? '', etc.) with
direct computed values — the custom comparator already gates
re-renders, so these memos only add closure/dep-array overhead
without ever providing cache hits
- Remove unused React import
* test: add unit tests for MessageIcon arePropsEqual comparator
Exercise the custom memo comparator to ensure:
- New object references with same display fields are treated as equal
- Changed name or avatar filepath triggers re-render
- iconData reference change triggers re-render
- undefined→defined agent with undefined fields is treated as equal
* fix: replace nested ternary with if-else for eslint compliance
* test: add comment on subtle equality invariant and defined→undefined case
* perf: compare iconData by field values instead of reference
iconData is a useMemo'd object from the parent, but comparing by
reference still causes unnecessary re-renders when the parent
recomputes the memo with identical primitive values. Compare all
five fields individually (endpoint, model, iconURL, modelLabel,
isCreatedByUser) for consistency with how agent/assistant are handled.
* 🔧 chore: bump @librechat/agents to v3.1.63
* 🔧 chore: update axios dependency to exact version 1.13.6
* 🔧 chore: update @aws-sdk/client-bedrock-runtime to version 3.1013.0 in package.json and package-lock.json
- Bump the version of @aws-sdk/client-bedrock-runtime across package.json files in api and packages/api to ensure compatibility with the latest features and fixes.
- Reflect the updated version in package-lock.json to maintain consistency in dependency resolution.
* 🔧 chore: update axios dependency to version 1.13.6 across multiple package.json and package-lock.json files
- Bump axios version from ^1.13.5 to 1.13.6 in package.json and package-lock.json for improved performance and security.
- Ensure consistency in dependency resolution across the project by updating all relevant files.
* chore: Update Handlebars and package versions in package-lock.json and package.json
- Upgrade Handlebars from version 4.7.7 to 4.7.9 in both package-lock.json and package.json for improved performance and security.
- Update librechat-data-provider version from 0.8.401 to 0.8.406 in package-lock.json.
- Update @librechat/data-schemas version from 0.0.40 to 0.0.48 in package-lock.json.
* chore: Upgrade @happy-dom/jest-environment and happy-dom versions in package-lock.json and package.json
- Update @happy-dom/jest-environment from version 20.8.3 to 20.8.9 for improved compatibility.
- Upgrade happy-dom from version 20.8.3 to 20.8.9 to ensure consistency across dependencies.
* chore: Upgrade @rollup/plugin-terser to version 1.0.0 in package-lock.json and package.json
- Update @rollup/plugin-terser from version 0.4.4 to 1.0.0 in both package-lock.json and package.json for improved performance and compatibility.
- Reflect the new version in the dependencies of data-provider and data-schemas packages.
* chore: Upgrade rollup-plugin-typescript2 to version 0.37.0 in package-lock.json and package.json
- Update rollup-plugin-typescript2 from version 0.35.0 to 0.37.0 in package-lock.json and all relevant package.json files for improved compatibility and performance.
- Adjust dependencies for semver and tslib to their latest versions in line with the rollup-plugin-typescript2 upgrade.
* chore: Upgrade nodemailer to version 8.0.4 in package-lock.json and package.json
- Update nodemailer from version 7.0.11 to 8.0.4 in both package-lock.json and package.json to enhance functionality and security.
* chore: Upgrade picomatch, yaml, brace-expansion versions in package-lock.json
- Update picomatch from version 4.0.3 to 4.0.4 across multiple dependencies for improved functionality.
- Upgrade brace-expansion from version 2.0.2 to 2.0.3 and from 5.0.3 to 5.0.5 to enhance compatibility and performance.
- Update yaml from version 1.10.2 to 1.10.3 for better stability.
Add INTERFACE_PERMISSION_FIELDS set defining the interface fields that
seed role permissions at startup (prompts, agents, marketplace, etc.).
These fields are now stripped from DB config overrides in the merge
layer because updateInterfacePermissions() only runs at boot — DB
overrides for these fields create a client/server permission mismatch.
Pure UI fields (endpointsMenu, modelSelect, parameters, presets,
sidePanel, customWelcome, etc.) continue to work in overrides as
before.
YAML startup path is completely unaffected.
* feat: add admin user management endpoints
Add /api/admin/users with list, search, and delete handlers gated by
ACCESS_ADMIN + READ_USERS/MANAGE_USERS system grants. Handler factory
in packages/api uses findUsers, countUsers, and deleteUserById from
data-schemas.
* fix: address convention violations in admin users handlers
* fix: add pagination, self-deletion guard, and DB-level search limit
- listUsers now uses parsePagination + countUsers for proper pagination
matching the roles/groups pattern
- findUsers extended with optional limit/offset options
- deleteUser returns 403 when caller tries to delete own account
- searchUsers passes limit to DB query instead of fetching all and
slicing in JS
- Fix import ordering per CLAUDE.md, complete logger mock
- Replace fabricated date fallback with undefined
* fix: deterministic sort, null-safe pagination, consistent search filter
- Add sort option to findUsers; listUsers sorts by createdAt desc for
deterministic pagination
- Use != null guards for offset/limit to handle zero values correctly
- Remove username from search filter since it is not in the projection
or AdminUserSearchResult response type
* fix: last-admin deletion guard and search query max-length
- Prevent deleting the last admin user (look up target role, count
admins, reject with 400 if count <= 1)
- Cap search query at 200 characters to prevent regex DoS
- Add tests for both guards
* fix: include missing capability name in 403 Forbidden response
* fix: cascade user deletion cleanup, search username, parallel capability checks
- Cascade Config, AclEntry, and SystemGrant cleanup on user deletion
(matching the pattern in roles/groups handlers)
- Add username to admin search $or filter for parity with searchUsers
- Parallelize READ_* capability checks in listAllGrants with Promise.all
* fix: TOCTOU safety net, capability info leak, DRY/style cleanup, data-layer tests
- Add post-delete admin recount with CRITICAL log if race leaves 0 admins
- Revert capability name from 403 response to server-side log only
- Document thin deleteUserById limitation (full cascade is a future task)
- DRY: extract query.trim() to local variable in searchUsersHandler
- Add username to search projection, response type, and AdminUserSearchResult
- Functional filter/map in grants.ts parallel capability check
- Consistent null guards and limit>0 guard in findUsers options
- Fallback for empty result.message on delete response
- Fix mockUser() to generate unique _id per call
- Break long destructuring across multiple lines
- Assert countUsers filter and non-admin skip in delete tests
- Add data-layer tests for findUsers limit, offset, sort, and pagination
* chore: comment out admin delete user endpoint (out of scope)
* fix: cast USER principalId to ObjectId for ACL entry cleanup
ACL entries store USER principalId as ObjectId (via grantPermission casting),
but deleteAclEntries is a raw deleteMany that passes the filter through.
Passing a string won't match stored ObjectIds, leaving orphaned entries.
* chore: comment out unused requireManageUsers alongside disabled delete route
* fix: add missing logger.warn mock in capabilities test
* fix: harden admin users handlers — type safety, response consistency, test coverage
- Unify response shape: AdminUserSearchResult.userId → id, add AdminUserListItem type
- Fix unsafe req.query type assertion in searchUsersHandler (typeof guards)
- Anchor search regex with ^ for prefix matching (enables index usage)
- Add total/capped to search response for truncation signaling
- Add parseInt radix, remove redundant new Date() wrap
- Add tests: countUsers throw, countUsers call args, array query param, capped flag
* fix: scope deleteGrantsForPrincipal to tenant, deterministic search sort, align test mocks
- Add tenantId option to AdminUsersDeps.deleteGrantsForPrincipal and
pass req.user.tenantId at the call site, matching the pattern already
used by the roles and groups handlers
- Add sort: { name: 1 } to searchUsersHandler for deterministic results
- Align test mock deleteUserById messages with production output
('User was deleted successfully.')
- Make capped-results test explicitly set limit: '20' instead of
relying on the implicit default
* test: add tenantId propagation test for deleteGrantsForPrincipal
Add tenantId to createReqRes user type and test that a non-undefined
tenantId is threaded through to deleteGrantsForPrincipal.
* test: remove redundant deleteUserById override in tenantId test
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: Lazy-initialize balance record when missing at check time
When balance is configured via admin panel DB overrides, users with
existing sessions never pass through the login middleware that creates
their balance record. This causes checkBalanceRecord to find no record
and return balance: 0, blocking the user.
Add optional balanceConfig and upsertBalanceFields deps to
CheckBalanceDeps. When no balance record exists but startBalance is
configured, lazily create the record instead of returning canSpend: false.
Pass the new deps from BaseClient, chatV1, and chatV2 callers.
* test: Add checkBalance lazy initialization tests
Cover lazy balance init scenarios: successful init with startBalance,
insufficient startBalance, missing config fallback, undefined
startBalance, missing upsertBalanceFields dep, and startBalance of 0.
* fix: Address review findings for lazy balance initialization
- Use canonical BalanceConfig and IBalanceUpdate types from
@librechat/data-schemas instead of inline type definitions
- Include auto-refill fields (autoRefillEnabled, refillIntervalValue,
refillIntervalUnit, refillAmount, lastRefill) during lazy init,
mirroring the login middleware's buildUpdateFields logic
- Add try/catch around upsertBalanceFields with graceful fallback to
canSpend: false on DB errors
- Read balance from DB return value instead of raw startBalance constant
- Fix misleading test names to describe observable throw behavior
- Add tests: upsertBalanceFields rejection, auto-refill field inclusion,
DB-returned balance value, and logViolation assertions
* fix: Address second review pass findings
- Fix import ordering: package type imports before local type imports
- Remove misleading comment on DB-fallback test, rename for clarity
- Add logViolation assertion to insufficient-balance lazy-init test
- Add test for partial auto-refill config (autoRefillEnabled without
required dependent fields)
* refactor: Replace createMockReqRes factory with describe-scoped consts
Replace zero-argument factory with plain const declarations using
direct type casts instead of double-cast through unknown.
* fix: Sort local type imports longest-first, add missing logViolation assertion
- Reorder local type imports in spec file per AGENTS.md (longest to
shortest within sub-group)
- Add logViolation assertion to startBalance: 0 test for consistent
violation payload coverage across all throw paths
* refactor: Add existingUsersOnly support to social and SAML auth callbacks
- Add `existingUsersOnly` option to the `socialLogin` handler factory to
reject unknown users instead of creating new accounts
- Refactor SAML strategy callback into `createSamlCallback(existingUsersOnly)`
factory function, mirroring the OpenID `createOpenIDCallback` pattern
- Extract shared SAML config into `getBaseSamlConfig()` helper
- Register `samlAdmin` passport strategy with `existingUsersOnly: true` and
admin-specific callback URL, called automatically from `setupSaml()`
* feat: Register admin OAuth strategy variants for all social providers
- Add admin strategy exports to Google, GitHub, Discord, Facebook, and
Apple strategy files with admin callback URLs and existingUsersOnly
- Extract provider configs into reusable helpers to avoid duplication
between regular and admin strategy constructors
- Re-export all admin strategy factories from strategies/index.js
- Register admin passport strategies (googleAdmin, githubAdmin, etc.)
alongside regular ones in socialLogins.js when env vars are present
* feat: Add admin auth routes for SAML and social OAuth providers
- Add initiation and callback routes for SAML, Google, GitHub, Discord,
Facebook, and Apple to the admin auth router
- Each provider follows the exchange code + PKCE pattern established by
OpenID admin auth: store PKCE challenge on initiation, retrieve on
callback, generate exchange code for the admin panel
- SAML and Apple use POST callbacks with state extracted from
req.body.RelayState and req.body.state respectively
- Extract storePkceChallenge(), retrievePkceChallenge(), and
generateState() helpers; refactor existing OpenID routes to use them
- All callback chains enforce requireAdminAccess, setBalanceConfig,
checkDomainAllowed, and the shared createOAuthHandler
- No changes needed to the generic POST /oauth/exchange endpoint
* fix: Update SAML strategy test to handle dual strategy registration
setupSaml() now registers both 'saml' and 'samlAdmin' strategies,
causing the SamlStrategy mock to be called twice. The verifyCallback
variable was getting overwritten with the admin callback (which has
existingUsersOnly: true), making all new-user tests fail.
Fix: capture only the first callback per setupSaml() call and reset
between tests.
* fix: Address review findings for admin OAuth strategy changes
- Fix existingUsersOnly rejection in socialLogin.js to use
cb(null, false, { message }) instead of cb(error), ensuring
passport's failureRedirect fires correctly for admin flows
- Consolidate duplicate require() calls in strategies/index.js by
destructuring admin exports from the already-imported default export
- Pass pre-parsed baseConfig to setupSamlAdmin() to avoid redundant
certificate file I/O at startup
- Extract getGoogleConfig() helper in googleStrategy.js for consistency
with all other provider strategy files
- Replace randomState() (openid-client) with generateState() (crypto)
in the OpenID admin route for consistency with all other providers,
and remove the now-unused openid-client import
* Reorder import statements in auth.js
* fix(auth): add origin binding to admin OAuth exchange codes
Bind admin OAuth exchange codes to the admin panel's origin at
generation time and validate the origin on redemption. This prevents
an intercepted code (via referrer leakage, logs, or network capture)
from being redeemed by a different origin within the 30-second TTL.
- Store the admin panel origin alongside the exchange code in cache
- Extract the request origin (from Origin/Referer headers) on the
exchange endpoint and pass it for validation
- Reject code redemption when the request origin does not match the
stored origin (code is still consumed to prevent replay)
- Backward compatible: codes without a stored origin are accepted
* fix(auth): add PKCE proof-of-possession to admin OAuth exchange codes
Add a PKCE-like code_challenge/code_verifier flow to the admin OAuth
exchange so that intercepting the exchange code alone is insufficient
to redeem it. The admin panel generates a code_verifier (stored in its
HttpOnly session cookie) and sends sha256(verifier) as code_challenge
through the OAuth initiation URL. LibreChat stores the challenge keyed
by OAuth state and attaches it to the exchange code. On redemption, the
admin panel sends the verifier and LibreChat verifies the hash match.
- Add verifyCodeChallenge() helper using SHA-256
- Store code_challenge in ADMIN_OAUTH_EXCHANGE cache (pkce: prefix, 5min TTL)
- Capture OAuth state in callback middleware before passport processes it
- Accept code_verifier in exchange endpoint body
- Backward compatible: no challenge stored → PKCE check skipped
* fix(auth): harden PKCE and origin binding in admin OAuth exchange
- Await cache.set for PKCE challenge storage with error handling
- Use crypto.timingSafeEqual for PKCE hash comparison
- Drop case-insensitive flag from hex validation regexes
- Add code_verifier length validation (max 512 chars)
- Normalize Origin header via URL parsing in resolveRequestOrigin
- Add test for undefined requestOrigin rejection
- Clarify JSDoc: hex-encoded SHA-256, not RFC 7636 S256
* fix(auth): fail closed on PKCE callback cache errors, clean up origin/buffer handling
- Callback middleware now redirects to error URL on cache.get failure
instead of silently continuing without PKCE challenge
- resolveRequestOrigin returns undefined (not raw header) on parse failure
- Remove dead try/catch around Buffer.from which never throws for string input
* chore(auth): remove narration comments, scope eslint-disable to lines
* chore(auth): narrow query.state to string, remove narration comments in exchange.ts
* fix(auth): address review findings — warn on missing PKCE challenge, validate verifier length, deduplicate URL parse
- Log warning when OAuth state is present but no PKCE challenge found
- Add minimum length check (>= 1) on code_verifier input validation
- Update POST /oauth/exchange JSDoc to document code_verifier param
- Deduplicate new URL(redirectUri) parse in createOAuthHandler
- Restore intent comment on pre-delete pattern in exchangeAdminCode
* test(auth): replace mock cache with real Keyv, remove all as-any casts
- Use real Keyv in-memory store instead of hand-rolled Map mock
- Replace jest.fn mocks with jest.spyOn on real Keyv instance
- Remove redundant store.has() assertion, use cache.get() instead
- Eliminate all eslint-disable and as-any suppressions
- User fixture no longer needs any cast (Keyv accepts plain objects)
* fix(auth): add IUser type cast for test fixture to satisfy tsc
* 📄 fix: Model-Aware Bedrock Document Size Validation
Remove the hard 4.5MB clamp on Bedrock document uploads so that
Claude 4+ (PDF) and Nova (PDF/DOCX) models can accept larger files
per AWS documentation. The default 4.5MB limit is preserved for
other models/formats, and fileConfig can now override it in either
direction—consistent with every other provider.
* address review: restore Math.min for non-exempt docs, tighten regexes, add tests
- Restore Math.min clamp for non-exempt Bedrock documents (fileConfig can
only lower the hard 4.5 MB API limit, not raise it); only exempt models
(Claude 4+ PDF, Nova PDF/DOCX) use ?? to allow fileConfig override
- Replace copied isBedrockClaude4Plus regex with cleaner anchored pattern
that correctly handles multi-digit version numbers (e.g. sonnet-40)
and removes dead Alt 1 branch matching no real Bedrock model IDs
- Tighten isBedrockNova from includes() to startsWith() to prevent
substring matching in unexpected positions
- Add integration test verifying model is threaded to validateBedrockDocument
- Add boundary tests for exempt + low configuredFileSizeLimit, non-exempt
+ high configuredFileSizeLimit, and exempt model accepting files up to 32 MB
- Revert two tests that were incorrectly inverted to prove wrong behavior
- Fix inaccurate JSDoc and misleading test name
* simplify: allow fileConfig to override Bedrock limit in either direction
Make Bedrock consistent with all other providers — fileConfig sets the
effective limit unconditionally via ?? rather than clamping with Math.min.
The model-aware defaults (4.5 MB for non-exempt, 32 MB for exempt) remain
as sensible fallbacks when no fileConfig is set.
* fix: handle cross-region inference profile IDs in Bedrock model matchers
Bedrock cross-region inference profiles prepend a region code to the
model ID (e.g. "us.amazon.nova-pro-v1:0", "eu.anthropic.claude-sonnet-4-...").
Both isBedrockNova and isBedrockClaude4Plus would miss these prefixed IDs,
silently falling back to the 4.5 MB default for eligible models.
Switch both matchers to use (?:^|\.) to anchor the vendor segment so the
pattern matches with or without a leading region prefix.
* ♻️ refactor: Remove redundant scopedCacheKey caching, support user-provided key model fetching
Remove redundant cache layers that used `scopedCacheKey()` (tenant-only scoping)
on top of `getAppConfig()` which already caches per-principal (role+user+tenant).
This caused config overrides for different principals within the same tenant to
be invisible due to stale cached data.
Changes:
- Add `requireJwtAuth` to `/api/endpoints` route for proper user context
- Remove ENDPOINT_CONFIG, STARTUP_CONFIG, PLUGINS, TOOLS, and MODELS_CONFIG
cache layers — all derive from `getAppConfig()` with cheap computation
- Enhance MODEL_QUERIES cache: hash(baseURL+apiKey) keys, 2-minute TTL,
caching centralized in `fetchModels()` base function
- Support fetching models with user-provided API keys in `loadConfigModels`
via `getUserKeyValues` lookup (no caching for user keys)
- Update all affected tests
Closes#1028
* ♻️ refactor: Migrate config services to TypeScript in packages/api
Move core config logic from CJS /api wrappers to typed TypeScript in
packages/api using dependency injection factories:
- `createEndpointsConfigService` — endpoint config merging + checkCapability
- `createLoadConfigModels` — custom endpoint model loading with user key support
- `createMCPToolCacheService` — MCP tool cache operations (update, merge, cache)
/api files become thin wrappers that wire dependencies (getAppConfig,
loadDefaultEndpointsConfig, getUserKeyValues, getCachedTools, etc.)
into the typed factories.
Also moves existing `endpoints/config.ts` → `endpoints/config/providers.ts`
to accommodate the new `config/` directory structure.
* 🔄 fix: Invalidate models query when user API key is set or revoked
Without this, users had to refresh the page after entering their API key
to see the updated model list fetched with their credentials.
- Invalidate QueryKeys.models in useUpdateUserKeysMutation onSuccess
- Invalidate QueryKeys.models in useRevokeUserKeyMutation onSuccess
- Invalidate QueryKeys.models in useRevokeAllUserKeysMutation onSuccess
* 🗺️ fix: Remap YAML-level override keys to AppConfig equivalents in mergeConfigOverrides
Config overrides stored in the DB use YAML-level keys (TCustomConfig),
but they're merged into the already-processed AppConfig where some fields
have been renamed by AppService. This caused mcpServers overrides to land
on a nonexistent key instead of mcpConfig, so config-override MCP servers
never appeared in the UI.
- Add OVERRIDE_KEY_MAP to remap mcpServers→mcpConfig, interface→interfaceConfig
- Apply remapping before deep merge in mergeConfigOverrides
- Add test for YAML-level key remapping behavior
- Update existing tests to use AppConfig field names in assertions
* 🧪 test: Update service.spec to use AppConfig field names after override key remapping
* 🛡️ fix: Address code review findings — reliability, types, tests, and performance
- Pass tenant context (getTenantId) in importers.js getEndpointsConfig call
- Add 5 tests for user-provided API key model fetching (key found, no key,
DB error, missing userId, apiKey-only with fixed baseURL)
- Distinguish NO_USER_KEY (debug) from infrastructure errors (warn) in catch
- Switch fetchPromisesMap from Promise.all to Promise.allSettled so one
failing provider doesn't kill the entire model config
- Parallelize getUserKeyValues DB lookups via batched Promise.allSettled
instead of sequential awaits in the loop
- Hoist standardCache instance in fetchModels to avoid double instantiation
- Replace Record<string, unknown> types with Partial<TConfig>-based types;
remove as unknown as T double-cast in endpoints config
- Narrow Bedrock availableRegions to typed destructure
- Narrow version field from string|number|undefined to string|undefined
- Fix import ordering in mcp/tools.ts and config/models.ts per AGENTS.md
- Add JSDoc to getModelsConfig alias clarifying caching semantics
* fix: Guard against null getCachedTools in mergeAppTools
* 🔍 fix: Address follow-up review — deduplicate extractEnvVariable, fix error discrimination, add log-level tests
- Deduplicate extractEnvVariable calls: resolve apiKey/baseURL once, reuse
for both the entry and isUserProvided checks (Finding A)
- Move ResolvedEndpoint interface from function closure to module scope (Finding B)
- Replace fragile msg.includes('NO_USER_KEY') with ErrorTypes.NO_USER_KEY
enum check against actual error message format (Finding C). Also handle
ErrorTypes.INVALID_USER_KEY as an expected "no key" case.
- Add test asserting logger.warn is called for infra errors (not debug)
- Add test asserting logger.debug is called for NO_USER_KEY errors (not warn)
* fix: Preserve numeric assistants version via String() coercion
* 🐛 fix: Address secondary review — Ollama cache bypass, cache tests, type safety
- Fix Ollama success path bypassing cache write in fetchModels (CRITICAL):
store result before returning so Ollama models benefit from 2-minute TTL
- Add 4 fetchModels cache behavior tests: cache write with TTL, cache hit
short-circuits HTTP, skipCache bypasses read+write, empty results not cached
- Type-safe OVERRIDE_KEY_MAP: Partial<Record<keyof TCustomConfig, keyof AppConfig>>
so compiler catches future field rename mismatches
- Fix import ordering in config/models.ts (package types longest→shortest)
- Rename ToolCacheDeps → MCPToolCacheDeps for naming consistency
- Expand getModelsConfig JSDoc to explain caching granularity
* fix: Narrow OVERRIDE_KEY_MAP index to satisfy strict tsconfig
* 🧩 fix: Add allowedProviders to TConfig, remove Record<string, unknown> from PartialEndpointEntry
The agents endpoint config includes allowedProviders (used by the frontend
AgentPanel to filter available providers), but it was missing from TConfig.
This forced PartialEndpointEntry to use & Record<string, unknown> as an
escape hatch, violating AGENTS.md type policy.
- Add allowedProviders?: (string | EModelEndpoint)[] to TConfig
- Remove Record<string, unknown> from PartialEndpointEntry — now just Partial<TConfig>
* 🛡️ fix: Isolate Ollama cache write from fetch try-catch, add Ollama cache tests
- Separate Ollama fetch and cache write into distinct scopes so a cache
failure (e.g., Redis down) doesn't misattribute the error as an Ollama
API failure and fall through to the OpenAI-compatible path (Issue A)
- Add 2 Ollama-specific cache tests: models written with TTL on fetch,
cached models returned without hitting server (Issue B)
- Replace hardcoded 120000 with Time.TWO_MINUTES constant in cache TTL
test assertion (Issue C)
- Fix OVERRIDE_KEY_MAP JSDoc to accurately describe runtime vs compile-time
type enforcement (Issue D)
- Add global beforeEach for cache mock reset to prevent cross-test leakage
* 🧪 fix: Address third review — DI consistency, cache key width, MCP tests
- Inject loadCustomEndpointsConfig via EndpointsConfigDeps with default
fallback, matching loadDefaultEndpointsConfig DI pattern (Finding 3)
- Widen modelsCacheKey from 64-bit (.slice(0,16)) to 128-bit (.slice(0,32))
for collision-sensitive cross-credential cache key (Finding 4)
- Add fetchModels.mockReset() in loadConfigModels.spec beforeEach to
prevent mock implementation leaks across tests (Finding 5)
- Add 11 unit tests for createMCPToolCacheService covering all three
functions: null/empty input, successful ops, error propagation,
cold-cache merge (Finding 2)
- Simplify getModelsConfig JSDoc to @see reference (Finding 10)
* ♻️ refactor: Address remaining follow-ups from reviews
OVERRIDE_KEY_MAP completeness:
- Add missing turnstile→turnstileConfig mapping
- Add exhaustiveness test verifying all three renamed keys are remapped
and original YAML keys don't leak through
Import role context:
- Pass userRole through importConversations job → importLibreChatConvo
so role-based endpoint overrides are honored during conversation import
- Update convos.js route to include req.user.role in the job payload
createEndpointsConfigService unit tests:
- Add 8 tests covering: default+custom merge, Azure/AzureAssistants/
Anthropic Vertex/Bedrock config enrichment, assistants version
coercion, agents allowedProviders, req.config bypass
Plugins/tools efficiency:
- Use Set for includedTools/filteredTools lookups (O(1) vs O(n) per plugin)
- Combine auth check + filter into single pass (eliminates intermediate array)
- Pre-compute toolDefKeys Set for O(1) tool definition lookups
* fix: Scope model query cache by user when userIdQuery is enabled
* fix: Skip model cache for userIdQuery endpoints, fix endpoints test types
- When userIdQuery is true, skip caching entirely (like user_provided keys)
to avoid cross-user model list leakage without duplicating cache data
- Fix AgentCapabilities type error in endpoints.spec.ts — use enum values
and appConfig() helper for partial mock typing
* 🐛 fix: Restore filteredTools+includedTools composition, add checkCapability tests
- Fix filteredTools regression: whitelist and blacklist are now applied
independently (two flat guards), matching original behavior where
includedTools=['a','b'] + filteredTools=['b'] produces ['a'] (Finding A)
- Fix Set spread in toolkit loop: pre-compute toolDefKeysList array once
alongside the Set, reuse for .some() without per-plugin allocation (Finding B)
- Add 2 filteredTools tests: blacklist-only path and combined
whitelist+blacklist composition (Finding C)
- Add 3 checkCapability tests: capability present, capability absent,
fallback to defaultAgentCapabilities for non-agents endpoints (Finding D)
* 🔑 fix: Include config-override MCP servers in filterAuthorizedTools
Config-override MCP servers (defined via admin config overrides for
roles/groups) were rejected by filterAuthorizedTools because it called
getAllServerConfigs(userId) without the configServers parameter. Only
YAML and DB-backed user servers were included in the access check.
- Add configServers parameter to filterAuthorizedTools
- Resolve config servers via resolveConfigServers(req) at all 4 callsites
(create, update, duplicate, revert) using parallel Promise.all
- Pass configServers through to getAllServerConfigs(userId, configServers)
so the registry merges config-source servers into the access check
- Update filterAuthorizedTools.spec.js mock for resolveConfigServers
* fix: Skip model cache for userIdQuery endpoints, fix endpoints test types
For user-provided key endpoints (userProvide: true), skip the full model
list re-fetch during message validation — the user already selected from
a list we served them, and re-fetching with skipCache:true on every
message send is both slow and fragile (5s provider timeout = rejected model).
Instead, validate the model string format only:
- Must be a string, max 256 chars
- Must match [a-zA-Z0-9][a-zA-Z0-9_.:\-/@+ ]* (covers all known provider
model ID formats while rejecting injection attempts)
System-configured endpoints still get full model list validation as before.
* 🧪 test: Add regression tests for filterAuthorizedTools configServers and validateModel
filterAuthorizedTools:
- Add test verifying configServers is passed to getAllServerConfigs and
config-override server tools are allowed through
- Guard resolveConfigServers in createAgentHandler to only run when
MCP tools are present (skip for tool-free agent creates)
validateModel (12 new tests):
- Format validation: missing model, non-string, length overflow, leading
special char, script injection, standard model ID acceptance
- userProvide early-return: next() called immediately, getModelsConfig
not invoked (regression guard for the exact bug this fixes)
- System endpoint list validation: reject unknown model, accept known
model, handle null/missing models config
Also fix unnecessary backslash escape in MODEL_PATTERN regex.
* 🧹 fix: Remove space from MODEL_PATTERN, trim input, clean up nits
- Remove space character from MODEL_PATTERN regex — no real model ID
uses spaces; prevents spurious violation logs from whitespace artifacts
- Add model.trim() before validation to handle accidental whitespace
- Remove redundant filterUniquePlugins call on already-deduplicated output
- Add comment documenting intentional whitelist+blacklist composition
- Add getUserKeyValues.mockReset() in loadConfigModels.spec beforeEach
- Remove narrating JSDoc from getModelsConfig one-liner
- Add 2 tests: trim whitespace handling, reject spaces in model ID
* fix: Match startup tool loader semantics — includedTools takes precedence over filteredTools
The startup tool loader (loadAndFormatTools) explicitly ignores
filteredTools when includedTools is set, with a warning log. The
PluginController was applying both independently, creating inconsistent
behavior where the same config produced different results at startup
vs plugin listing time.
Restored mutually exclusive semantics: when includedTools is non-empty,
filteredTools is not evaluated.
* 🧹 chore: Simplify validateModel flow, note auth requirement on endpoints route
- Separate missing-model from invalid-model checks cleanly: type+presence
guard first, then trim+format guard (reviewer NIT)
- Add route comment noting auth is required for role/tenant scoping
* fix: Write trimmed model back to req.body.model for downstream consumers
* feat: add System Grants handler factory with tests
Handler factory with 4 endpoints: getEffectiveCapabilities (expanded
capability set for authenticated user), getPrincipalGrants (list grants
for a specific principal), assignGrant, and revokeGrant. Write ops
dynamically check MANAGE_ROLES/GROUPS/USERS based on target principal
type. 31 unit tests covering happy paths, validation, 403, and errors.
* feat: wire System Grants REST routes
Mount /api/admin/grants with requireJwtAuth + ACCESS_ADMIN gate.
Add barrel export for createAdminGrantsHandlers and AdminGrantsDeps.
* fix: cascade grant cleanup on role deletion
Add deleteGrantsForPrincipal to AdminRolesDeps and call it in
deleteRoleHandler via Promise.allSettled after successful deletion,
matching the groups cleanup pattern. 3 tests added for cleanup call,
skip on 404, and resilience to cleanup failure.
* fix: simplify cascade grant cleanup on role deletion
Replace Promise.allSettled wrapper with a direct try/catch for the
single deleteGrantsForPrincipal call.
* fix: harden grant handlers with auth, validation, types, and RESTful revoke
- Add per-handler auth checks (401) and granular capability gates
(READ_* for getPrincipalGrants, possession check for assignGrant)
- Extract validatePrincipal helper; rewrite validateGrantBody to use
direct type checks instead of unsafe `as string` casts
- Align DI types with data layer (ResolvedPrincipal.principalType
widened to string, getUserPrincipals role made optional)
- Switch revoke route from DELETE body to RESTful URL params
- Return 201 for assignGrant to match roles/groups create convention
- Handle null grantCapability return with 500
- Add comprehensive test coverage for new auth/validation paths
* fix: deduplicate ResolvedPrincipal, typed body, defensive auth checks
- Remove duplicate ResolvedPrincipal from capabilities.ts; import the
canonical export from grants.ts
- Replace Record<string, unknown> with explicit GrantRequestBody interface
- Add defensive 403 when READ_CAPABILITY_BY_TYPE lookup misses
- Document revoke asymmetry (no possession check) with JSDoc
- Use _id only in resolveUser (avoid Mongoose virtual reliance)
- Improve null-grant error message
- Complete logger mock in tests
* refactor: move ResolvedPrincipal to shared types to fix circular dep
Extract ResolvedPrincipal from admin/grants.ts to types/principal.ts
so middleware/capabilities.ts imports from shared types rather than
depending upward on the admin handler layer.
* chore: remove dead re-export, align logger mocks across admin tests
- Remove unused ResolvedPrincipal re-export from grants.ts (canonical
source is types/principal.ts)
- Align logger mocks in roles.spec.ts and groups.spec.ts to include
all log levels (error, warn, info, debug) matching grants.spec.ts
* fix: cascade Config and AclEntry cleanup on role deletion
Add deleteConfig and deleteAclEntries to role deletion cascade,
matching the group deletion pattern. Previously only grants were
cleaned up, leaving orphaned config overrides and ACL entries.
* perf: single-query batch for getEffectiveCapabilities
Add getCapabilitiesForPrincipals (plural) to the data layer — a single
$or query across all principals instead of N+1 parallel queries. Wire
it into the grants handler so getEffectiveCapabilities hits the DB once
regardless of how many principals the user has.
* fix: defer SystemCapabilities access to factory call time
Move all SystemCapabilities usage (VALID_CAPABILITIES,
MANAGE_CAPABILITY_BY_TYPE, READ_CAPABILITY_BY_TYPE) inside the
createAdminGrantsHandlers factory. External test suites that mock
@librechat/data-schemas without providing SystemCapabilities crashed
at import time when grants.ts was loaded transitively.
* test: add data-layer and handler test coverage for review findings
- Add 6 mongodb-memory-server tests for getCapabilitiesForPrincipals:
multi-principal batch, empty array, filtering, tenant scoping
- Add handler test: all principals filtered (only PUBLIC)
- Add handler test: granting an implied capability succeeds
- Add handler test: all cascade cleanup operations fail simultaneously
- Document platform-scope-only tenantId behavior in JSDoc
* fix: resolveUser fallback to user.id, early-return empty principals
- Match capabilities middleware pattern: _id?.toString() ?? user.id
to handle JWT-deserialized users without Mongoose _id
- Move empty-array guard before principals.map() to skip unnecessary
normalizePrincipalId calls
- Add comment explaining VALID_PRINCIPAL_TYPES module-scope asymmetry
* refactor: derive VALID_PRINCIPAL_TYPES from capability maps
Make MANAGE_CAPABILITY_BY_TYPE and READ_CAPABILITY_BY_TYPE
non-Partial Records over a shared GrantPrincipalType union, then
derive VALID_PRINCIPAL_TYPES from the map keys. This makes divergence
between the three data structures structurally impossible.
* feat: add GET /api/admin/grants list-all-grants endpoint
Add listAllGrants data-layer method and handler so the admin panel
can fetch all grants in a single request instead of fanning out
N+M calls per role and group. Response is filtered to only include
grants for principal types the caller has read access to.
* fix: update principalType to use GrantPrincipalType for consistency in grants handling
- Refactor principalType in createAdminGrantsHandlers to use GrantPrincipalType instead of PrincipalType for better type accuracy.
- Ensure type consistency across the grants handling logic in the API.
* fix: address admin grants review findings — tenantId propagation, capability validation, pagination, and test coverage
Propagate tenantId through all grant operations for multi-tenancy support.
Extract isValidCapability to accept full SystemCapability union (base, section,
assign) and reuse it in both Mongoose schema validation and handler input checks.
Replace listAllGrants with paginated listGrants + countGrants. Filter PUBLIC
principals from getCapabilitiesForPrincipals queries. Export getCachedPrincipals
from ALS store for fast-path principal resolution. Move DELETE capability param
to query string to avoid colon-in-URL issues. Remove dead code and add
comprehensive handler and data-layer test coverage.
* refactor: harden admin grants — FilterQuery types, auth-first ordering, DELETE path param, isValidCapability tests
Replace Record<string, unknown> with FilterQuery<ISystemGrant> across all
data-layer query filters. Refactor buildTenantFilter to a pure tenantCondition
function that returns a composable FilterQuery fragment, eliminating the $or
collision between tenant and principal queries. Move auth check before input
validation in getPrincipalGrantsHandler, assignGrantHandler, and
revokeGrantHandler to avoid leaking valid type names to unauthenticated callers.
Switch DELETE route from query param back to path param (/:capability) with
encodeURIComponent per project conventions. Add compound index for listGrants
sort. Type VALID_PRINCIPAL_TYPES as Set<GrantPrincipalType>. Remove unused
GetCachedPrincipalsFn type export. Add dedicated isValidCapability unit tests
and revokeGrant idempotency test.
* refactor: batch capability checks in listGrantsHandler via getHeldCapabilities
Replace 3 parallel hasCapabilityForPrincipals DB calls with a single
getHeldCapabilities query that returns the subset of capabilities any
principal holds. Also: defensive limit(0) clamp, parallelized assignGrant
auth checks, principalId type-vs-required error split, tenantCondition
hoisted to factory top, JSDoc on cascade deps, DELETE route encoding note.
* fix: normalize principalId and filter undefined in getHeldCapabilities
Add normalizePrincipalId + null guard to getHeldCapabilities, matching
the contract of getCapabilitiesForPrincipals. Simplify allCaps build
with flatMap, add no-tenantId cross-check and undefined-principalId
test cases.
* refactor: use concrete types in GrantRequestBody, rename encoding test
Replace unknown fields with explicit string types in GrantRequestBody,
matching the established pattern in roles/groups/config handlers. Rename
misleading 'encoded' test to 'with colons' since Express auto-decodes
req.params.
* fix: support hierarchical parent capabilities in possession checks
hasCapabilityForPrincipals and getHeldCapabilities now resolve parent
base capabilities for section/assignment grants. An admin holding
manage:configs can now grant manage:configs:<section> and transitively
read:configs:<section>. Fixes anti-escalation 403 blocking config
capability delegation.
* perf: use getHeldCapabilities in assignGrant to halve DB round-trips
assignGrantHandler was making two parallel hasCapabilityForPrincipals
calls to check manage + capability possession. getHeldCapabilities was
introduced in this PR specifically for this pattern. Replace with a
single batched call. Update corresponding spec assertions.
* fix: validate role existence before granting capabilities
Grants for non-existent role names were silently persisted, creating
orphaned grants that could surprise-activate if a role with that name
was later created. Add optional checkRoleExists dep to assignGrant and
wire it to getRoleByName in the route file.
* refactor: tighten principalType typing and use grantCapability in tests
Narrow getCapabilitiesForPrincipals parameter from string to
PrincipalType, removing the redundant cast. Replace direct
SystemGrant.create() calls in getCapabilitiesForPrincipals tests with
methods.grantCapability() to honor the schema's normalization invariant.
Add getHeldCapabilities extended capability tests.
* test: rename misleading cascade cleanup test name
The test only injects failure into deleteGrantsForPrincipal, not all
cascade operations. Rename from 'cascade cleanup fails' to 'grant
cleanup fails' to match the actual scope.
* fix: reorder role check after permission guard, add tenantId to index
Move checkRoleExists after the getHeldCapabilities permission check so
that a sub-MANAGE_ROLES admin cannot probe role name existence via
400 vs 403 response codes.
Add tenantId to the { principalType, capability } index so listGrants
queries in multi-tenant deployments can use a covering index instead
of post-scanning for tenant condition.
Add missing test for checkRoleExists throwing.
* fix: scope deleteGrantsForPrincipal to tenant on role deletion
deleteGrantsForPrincipal previously filtered only on principalType +
principalId, deleting grants across all tenants. Since the role schema
supports multi-tenancy (compound unique index on name + tenantId), two
tenants can share a role name like 'editor'. Deleting that role in one
tenant would wipe grants for identically-named roles in other tenants.
Add optional tenantId parameter to deleteGrantsForPrincipal. When
provided, scopes the delete to that tenant plus platform-level grants.
Propagate req.user.tenantId through the role deletion cascade.
* fix: scope grant cleanup to tenant on group deletion
Same cross-tenant gap as the role deletion path: deleteGroupHandler
called deleteGrantsForPrincipal without tenantId, so deleting a group
would wipe its grants across all tenants. Extract req.user.tenantId
and pass it through.
* test: add HTTP integration test for admin grants routes
Supertest-based test with real MongoMemoryServer exercising the full
Express wiring: route registration, injected auth middleware, handler
DI deps, and real DB round-trips.
Covers GET /, GET /effective, POST / + DELETE / lifecycle, role
existence validation, and 401 for unauthenticated callers.
Also documents the expandImplications scope: the /effective endpoint
returns base-level capabilities only; section-level resolution is
handled at authorization check time by getParentCapabilities.
* fix: use exact tenant match in deleteGrantsForPrincipal, normalize principalId, harden API
CRITICAL: deleteGrantsForPrincipal was using tenantCondition (a
read-query helper) for deleteMany, which includes the
{ tenantId: { $exists: false } } arm. This silently destroyed
platform-level grants when a tenant-scoped role/group deletion
occurred. Replace with exact { tenantId } match for deletes so
platform-level grants survive tenant-scoped cascade cleanup.
Refactor deleteGrantsForPrincipal signature from fragile positional
overload (sessionOrTenantId union + maybeSession) to a clean options
object: { tenantId?, session? }. Update all callers and test assertions.
Add normalizePrincipalId to hasCapabilityForPrincipals to match the
pattern already used by getHeldCapabilities — prevents string/ObjectId
type mismatch on USER/GROUP principal queries.
Also: export GrantPrincipalType from barrel, add upper-bound cap to
listGrants, document GROUP/USER existence check trade-off, add
integration tests for tenant-isolation property of deleteGrantsForPrincipal.
* fix: forward tenantId to getUserPrincipals in resolvePrincipals
resolvePrincipals had tenantId available from the caller but only
forwarded it to getCachedPrincipals (cache lookup). The DB fallback
via getUserPrincipals omitted it. While the Group schema's
applyTenantIsolation Mongoose plugin handles scoping via
AsyncLocalStorage in HTTP request context, explicitly passing tenantId
makes the contract visible and prevents silent cross-tenant group
resolution if called outside request context.
* fix: remove unused import and add assertion to 401 integration test
Remove unused SystemCapabilities import flagged by ESLint. Add explicit
body assertion to the 401 test so it has a jest expect() call.
* chore: hoist grant limit constants to scope, remove dead isolateModules
Move GRANTS_DEFAULT_LIMIT / GRANTS_MAX_LIMIT from inside listGrants
function body to createSystemGrantMethods scope so they are evaluated
once at module load. Remove dead jest.isolateModules + jest.doMock
block in integration test — the ~/models mock was never exercised
since handlers are built with explicit DI deps.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* refactor: add TMessageChatContext type for stable context passing
Defines a type for a stable context object that wrapper components
pass to memo'd message components, avoiding direct ChatContext
subscriptions that bypass React.memo during streaming.
* perf: remove ChatContext subscription from useMessageActions
useMessageActions previously called useChatContext() inside memo'd
components (MessageRender, ContentRender), bypassing React.memo
when isSubmitting changed during streaming. Now accepts a stable
chatContext param instead, using a ref for the isSubmitting guard
in regenerateMessage.
Also stabilizes handleScroll in useMessageProcess by using a ref
for isSubmitting instead of including it in useCallback deps.
* perf: pass stable chatContext to memo'd message components
Wrapper components (Message, MessageContent) now create a stable
chatContext object via useMemo with a getter-backed isSubmitting,
and compute effectiveIsSubmitting (false for non-latest messages).
This ensures MessageRender and ContentRender (both React.memo'd)
only re-render for the latest message during streaming, preventing
unnecessary re-renders of all prior messages and their SubRow,
HoverButtons, and SiblingSwitch children.
* perf: add custom memo comparators to prevent message reference re-renders
buildTree creates new message objects on every streaming update for
ALL messages, not just the changed one. This defeats React.memo's
default shallow comparison since the message prop has a new reference
even when the content hasn't changed.
Custom areEqual comparators now compare message by key fields
(messageId, text, error, depth, children length, etc.) instead of
reference equality, preventing unnecessary re-renders of SubRow,
Files, HoverButtons and other children for non-latest messages.
* perf: memoize ChatForm children to prevent streaming re-renders
- Wrap StopButton in React.memo
- Wrap AudioRecorder in React.memo, use ref for isSubmitting in
onTranscriptionComplete callback to stabilize it
- Remove useChatContext() from FileFormChat (bypassed its memo
during streaming), accept files/setFiles/setFilesLoading as
props from ChatForm instead
* perf: stabilize ChatForm child props to prevent cascading re-renders
ChatForm re-renders frequently during streaming (ChatContext changes).
This caused StopButton and AttachFileChat/AttachFileMenu to re-render
despite being memo'd, because their props were new references each time.
- Wrap handleStopGenerating in a ref-based stable callback so StopButton
always receives the same function reference
- Create stableConversation via useMemo keyed on rendering-relevant
fields only (conversationId, endpoint, agent_id, etc.), so
AttachFileChat and FileFormChat don't re-render from unrelated
conversation metadata updates (e.g., title generation)
* perf: remove ChatContext subscription from AttachFileMenu and FileFormChat
Both components used useFileHandling() which internally calls
useChatContext(), bypassing their React.memo wrappers and causing
re-renders on every streaming chunk.
Switch to useFileHandlingNoChatContext() which accepts file state
as parameters. The state (files, setFiles, setFilesLoading,
conversation) is passed down from ChatForm → AttachFileChat →
AttachFileMenu as props, keeping the memo chain intact.
* fix: update imports and test mocks for useFileHandlingNoChatContext
- Re-export useFileHandlingNoChatContext from hooks barrel
- Import from ~/hooks instead of direct path for test compatibility
- Add useToastContext mock to @librechat/client in AttachFileMenu tests
since useFileHandlingNoChatContext runs the core hook which needs it
- Add useFileHandlingNoChatContext to ~/hooks test mock
* perf: fix remaining ChatForm streaming re-renders
- Switch AttachFileMenu from useSharePointFileHandling (subscribes to
ChatContext) to useSharePointFileHandlingNoChatContext with explicit
file state props
- Memoize ChatForm textarea onFocus/onBlur handlers with useCallback
to prevent TextareaAutosize re-renders (inline arrow functions and
.bind() created new references on every ChatForm render)
- Update AttachFileMenu test mocks for new hook variants
* refactor: add displayName to ChatForm for React DevTools
* perf: prevent ChatForm re-renders during streaming via wrapper pattern
ChatForm was re-rendering on every streaming chunk because it subscribed
to useChatContext() internally, and the ChatContext value changed
frequently during streaming.
Extract context subscription into a ChatFormWrapper that:
- Subscribes to useChatContext() (re-renders on every chunk, cheap)
- Stabilizes conversation via selective useMemo
- Stabilizes handleStopGenerating via ref-based callback
- Passes individual stable values as props to ChatForm
ChatForm (memo'd) now receives context values as props instead of
subscribing directly. Since individual values (files, setFiles,
isSubmitting, etc.) are stable references during streaming, ChatForm's
memo prevents re-renders entirely — it only re-renders when isSubmitting
actually toggles (2x per stream: start/end).
* perf: stabilize newConversation prop and memoize CollapseChat
- Wrap newConversation in ref-based stable callback in ChatFormWrapper
(was the remaining unstable prop causing ChatForm to re-render)
- Wrap CollapseChat in React.memo to prevent re-renders from parent
* perf: memoize useAddedResponse return value
useAddedResponse returned a new object literal on every render,
causing AddedChatContext.Provider to trigger re-renders of all
consumers (including ChatForm) on every streaming chunk. Wrap in
useMemo so the context value stays referentially stable.
* perf: memoize TextareaHeader to prevent re-renders from ChatForm
* perf: address review findings for streaming render optimization
Finding 1: Switch AttachFile.tsx from useFileHandling to
useFileHandlingNoChatContext, closing the optimization hole for
standard (non-agent) chat endpoints.
Finding 2: Replace content reference equality with length comparison
in both memo comparators — safer against buildTree array reconstruction.
Finding 3: Add conversation?.model to stableConversation deps in
ChatFormWrapper so file uploads use the correct model after switches.
Finding 4/14: Fix stableNewConversation to explicitly return the
underlying call's result instead of discarding it via `as` cast.
Finding 5/6: Extract useMemoizedChatContext hook shared by Message.tsx
and MessageContent.tsx — eliminates ~70 lines of duplication and
stabilizes chatContext.conversation via selective useMemo to prevent
post-stream metadata updates from re-rendering all messages.
Finding 8: Use TMessage type for regenerate param instead of
Record<string, unknown>.
Finding 9: Use FileSetter alias in FileFormChat instead of inline type.
Finding 11: Fix pre-existing broken throttle in useMessageProcess —
was creating a new throttle instance per call, providing zero
deduplication. Now retains the instance via useMemo.
Finding 12: Initialize isSubmittingRef with chatContext.isSubmitting
instead of false for consistency.
Finding 13: Add ChatFormWrapper displayName.
* fix: revert content comparison to reference equality in memo comparators
The length-based comparison (content?.length) missed updates within
existing content parts during streaming — text chunks update a part's
content without changing the array length, so the comparator returned
true and skipped re-renders for the latest message.
Reference equality (===) is correct here: buildTree preserves content
array references for unchanged messages via shallow spread, while
React Query gives the latest message a new reference when its content
updates during streaming.
* fix: cancel throttled handleScroll on unmount and remove unused import
* fix: use chatContext getter directly in regenerateMessage callback
The local isSubmittingRef was stale for non-latest messages (which
don't re-render during streaming by design). chatContext.isSubmitting
is a getter backed by the wrapper's ref, so reading it at call-time
always returns the current value regardless of whether the component
has re-rendered.
* fix: remove unused useCallback import from useMemoizedChatContext
* fix: pass global isSubmitting to HoverButtons for action gating
HoverButtons uses isSubmitting via useGenerationsByLatest to disable
regenerate and hide edit buttons during streaming. Passing the
effective value (false for non-latest messages) re-enabled those
actions mid-stream, risking overlapping edits/regenerations.
Use chatContext.isSubmitting (getter, always returns current value)
for HoverButtons while keeping the effective value for rendering-only
UI (cursor, placeholder, streaming indicator).
* fix: address second review — stale HoverButtons, messages dep, cleanup
- Add isSubmitting to chatContext useMemo deps in useMemoizedChatContext
so HoverButtons correctly updates when streaming starts/ends (2 extra
re-renders per session, belt-and-suspenders for post-stream state)
- Change conversation?.messages?.length dep to boolean in ChatFormWrapper
stableConversation — only need 0↔1+ transition for landing page check,
not exact count on every message addition
- Add defensive comment at chatContext destructuring point in
useMessageActions explaining why isSubmitting must not be destructured
- Remove dead mockUseFileHandling.mockReturnValue from AttachFileMenu tests
* chore: remove dead useFileHandling mock artifacts from AttachFileMenu tests
* fix: resolve eslint warnings for useMemo dependencies
- Extract complex expression (conversation?.messages?.length ?? 0) > 0
to hasMessages variable for static analysis in ChatFormWrapper
- Add eslint-disable for intentional isSubmitting dep in
useMemoizedChatContext (forces new chatContext reference on streaming
start/end so HoverButtons re-renders)
* refactor: Remove deprecated and unused fields from endpoint schemas
- Remove summarize, summaryModel from endpointSchema and azureEndpointSchema
- Remove plugins from azureEndpointSchema
- Remove customOrder from endpointSchema and azureEndpointSchema
- Remove baseURL from all and agents endpoint schemas
- Type paramDefinitions with full SettingDefinition-based schema
- Clean up summarize/summaryModel references in initialize.ts and config.spec.ts
* refactor: Improve MCP transport schema typing
- Add defaults to transport type discriminators (stdio, websocket, sse)
- Type stderr field as IOType union instead of z.any()
* refactor: Add narrowed preset schema for model specs
- Create tModelSpecPresetSchema omitting system/DB/deprecated fields
- Update tModelSpecSchema to use the narrowed preset schema
* test: Add explicit type field to MCP test fixtures
Add transport type discriminator to test objects that construct
MCPOptions/ParsedServerConfig directly, required after type field
changed from optional to default in schema definitions.
* chore: Bump librechat-data-provider to 0.8.404
* refactor: Tighten z.record(z.any()) fields to precise value types
- Type headers fields as z.record(z.string()) in endpoint, assistant, and azure schemas
- Type addParams as z.record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
- Type azure additionalHeaders as z.record(z.string())
- Type memory model_parameters as z.record(z.union([z.string(), z.number(), z.boolean()]))
- Type firecrawl changeTrackingOptions.schema as z.record(z.string())
* refactor: Type supportedMimeTypes schema as z.array(z.string())
Replace z.array(z.any()).refine() with z.array(z.string()) since config
input is always strings that get converted to RegExp via
convertStringsToRegex() after parsing. Destructure supportedMimeTypes
from spreads to avoid string[]/RegExp[] type mismatch.
* refactor: Tighten enum, role, and numeric constraint schemas
- Type engineSTT as enum ['openai', 'azureOpenAI']
- Type engineTTS as enum ['openai', 'azureOpenAI', 'elevenlabs', 'localai']
- Constrain playbackRate to 0.25–4 range
- Type titleMessageRole as enum ['system', 'user', 'assistant']
- Add int().nonnegative() to MCP timeout and firecrawl timeout
* chore: Bump librechat-data-provider to 0.8.405
* fix: Accept both string and RegExp in supportedMimeTypes schema
The schema must accept both string[] (config input) and RegExp[]
(post-merge runtime) since tests validate merged output against the
schema. Use z.union([z.string(), z.instanceof(RegExp)]) to handle both.
* refactor: Address review findings for schema tightening PR
- Revert changeTrackingOptions.schema to z.record(z.unknown()) (JSON Schema is nested, not flat strings)
- Remove dead contextStrategy code from BaseClient.js and cleanup.js
- Extract paramDefinitionSchema to named exported constant
- Add .int() constraint to columnSpan and columns
- Apply consistent .int().nonnegative() to initTimeout, sseReadTimeout, scraperTimeout
- Update stale stderr JSDoc to match actual accepted types
- Add comprehensive tests for paramDefinitionSchema, tModelSpecPresetSchema,
endpointSchema deprecated field stripping, and azureEndpointSchema
* fix: Address second review pass findings
- Revert supportedMimeTypesSchema to z.array(z.string()) and remove
as string[] casts — fix tests to not validate merged RegExp[] output
against the config input schema
- Remove unused tModelSpecSchema import from test file
- Consolidate duplicate '../src/schemas' imports
- Add expiredAt coverage to tModelSpecPresetSchema test
- Assert plugins is absent in azureEndpointSchema test
- Add sync comments for engineSTT/engineTTS enum literals
* refactor: Omit preset-management fields from tModelSpecPresetSchema
Omit conversationId, presetId, title, defaultPreset, and order from the
model spec preset schema — these are preset-management fields that don't
belong in model spec configuration.
* chore: remove deprecated Gemini 2.0 models from default models list
Remove gemini-2.0-flash-001 and gemini-2.0-flash-lite from the Google
default models array, as they have been deprecated by Google.
Closes#12444
* fix: add mistral-large-3 max context tokens (256k)
Add mistral-large-3 with 255000 max context tokens to the mistralModels
map. Without this entry, the model falls back to the generic
mistral-large key (131k), causing context window errors when using
tools with Azure AI Foundry deployments.
Closes#12429
* test: add mistral-large-3 token resolution tests and fix key ordering
Add test coverage for mistral-large-3 context token resolution,
verifying exact match, suffixed variants, and longest-match precedence
over the generic mistral-large key. Reorder the mistral-large-3 entry
after mistral-large to follow the file's documented convention of
listing newer models last for reverse-scan performance.
* fix(data-schemas): resolve TypeScript strict type check errors in source files
- Constrain ConfigSection to string keys via `string & keyof TCustomConfig`
- Replace broken `z` import from data-provider with TCustomConfig derivation
- Add `_id: Types.ObjectId` to IUser matching other Document interfaces
- Add `federatedTokens` and `openidTokens` optional fields to IUser
- Type mongoose model accessors as `Model<IRole>` and `Model<IUser>`
- Widen `getPremiumRate` param to accept `number | null`
- Widen `bulkWriteAclEntries` ops to untyped `AnyBulkWriteOperation[]`
- Fix `getUserPrincipals` return type to use `PrincipalType` enum
- Add non-null assertions for `connection.db` in migration files
- Import DailyRotateFile constructor directly instead of relying on
broken module augmentation across mismatched node_modules trees
- Add winston-daily-rotate-file as devDependency for type resolution
* fix(data-schemas): resolve TypeScript type errors in test files
- Replace arbitrary test keys with valid TCustomConfig properties in config.spec
- Use non-null assertions for permission objects in role.methods.spec
- Replace `.SHARED_GLOBAL` access with `.not.toHaveProperty()` for legacy field
- Add non-null assertions for balance, writeRate, readRate in spendTokens.spec
- Update mock user _id to use ObjectId in user.test
- Remove unused Schema import in tenantIndexes.spec
* fix(api): resolve TypeScript strict type check errors across source and test files
- Widen getUserPrincipals dep type in capabilities middleware
- Fix federatedTokens type in createSafeUser return
- Use proper mock req type for read-only properties in preAuthTenant.spec
- Replace `as IUser` casts with ObjectId-typed mocks in openid/oidc specs
- Use TokenExchangeMethodEnum values instead of string literals in MCP specs
- Fix SessionStore type compatibility in sessionCache specs
- Replace `catch (error: any)` with `(error as Error)` in redis specs
- Remove invalid properties from test data in initialize and MCP specs
- Add String.prototype.isWellFormed declaration for sanitizeTitle spec
* fix(client): resolve TypeScript type errors in shared client components
- Add default values for destructured bindings in OGDialogTemplate
- Replace broken ExtendedFile import with inline type in FileIcon
* ci: add TypeScript type-check job to backend review workflow
Add a `typecheck` job that runs `tsc --noEmit` on all four TypeScript
workspaces (data-provider, data-schemas, @librechat/api, @librechat/client)
after the build step. Catches type errors that rollup builds may miss.
* fix(data-schemas): add local type declaration for DailyRotateFile transport
The `winston-daily-rotate-file` package ships a module augmentation for
`winston/lib/winston/transports`, but it fails when winston and
winston-daily-rotate-file resolve from different node_modules trees
(which happens in this monorepo due to npm hoisting).
Add a local `.d.ts` declaration that augments the same module path from
within data-schemas' compilation unit, so `tsc --noEmit` passes while
keeping the original runtime pattern (`new winston.transports.DailyRotateFile`).
* fix: address code review findings from PR #12451
- Restore typed `AnyBulkWriteOperation<AclEntry>[]` on bulkWriteAclEntries,
cast to untyped only at the tenantSafeBulkWrite call site (Finding 1)
- Type `findUser` model accessor consistently with `findUsers` (Finding 2)
- Replace inline `import('mongoose').ClientSession` with top-level import type
- Use `toHaveLength` for spy assertions in playwright-expect spec file
- Replace numbered Record casts with `.not.toHaveProperty()` in
role.methods.spec for SHARED_GLOBAL assertions
- Use per-test ObjectIds instead of shared testUserId in openid.spec
- Replace inline `import()` type annotations with top-level SessionData
import in sessionCache spec
- Remove extraneous blank line in user.ts searchUsers
* refactor: address remaining review findings (4–7)
- Extract OIDCTokens interface in user.ts; deduplicate across IUser fields
and oidc.ts FederatedTokens (Finding 4)
- Move String.isWellFormed declaration from spec file to project-level
src/types/es2024-string.d.ts (Finding 5)
- Replace verbose `= undefined` defaults in OGDialogTemplate with null
coalescing pattern (Finding 6)
- Replace `Record<string, unknown>` TestConfig with named interface
containing explicit test fields (Finding 7)
2026-03-28 21:06:39 -04:00
Marco BerettaGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* fix: wrap seedDatabase() in runAsSystem() for strict tenant mode
seedDatabase() was called without tenant context at startup, causing
every Mongoose operation inside it to throw when
TENANT_ISOLATION_STRICT=true. Wrapping in runAsSystem() gives it the
SYSTEM_TENANT_ID sentinel so the isolation plugin skips filtering,
matching the pattern already used for performStartupChecks and
updateInterfacePermissions.
* fix: chain tenantContextMiddleware in optionalJwtAuth
optionalJwtAuth populated req.user but never established ALS tenant
context, unlike requireJwtAuth which chains tenantContextMiddleware
after successful auth. Authenticated users hitting routes with
optionalJwtAuth (e.g. /api/banner) had no tenant isolation.
* feat: tenant-safe bulkWrite wrapper and call-site migration
Mongoose's bulkWrite() does not trigger schema-level middleware hooks,
so the applyTenantIsolation plugin cannot intercept it. This adds a
tenantSafeBulkWrite() utility that injects the current ALS tenant
context into every operation's filter/document before delegating to
native bulkWrite.
Migrates all 8 runtime bulkWrite call sites:
- agentCategory (seedCategories, ensureDefaultCategories)
- conversation (bulkSaveConvos)
- message (bulkSaveMessages)
- file (batchUpdateFiles)
- conversationTag (updateTagsForConversation, bulkIncrementTagCounts)
- aclEntry (bulkWriteAclEntries)
systemGrant.seedSystemGrants is intentionally not migrated — it uses
explicit tenantId: { $exists: false } filters and is exempt from the
isolation plugin.
* feat: pre-auth tenant middleware and tenant-scoped config cache
Adds preAuthTenantMiddleware that reads X-Tenant-Id from the request
header and wraps downstream in tenantStorage ALS context. Wired onto
/oauth, /api/auth, /api/config, and /api/share — unauthenticated
routes that need tenant scoping before JWT auth runs.
The /api/config cache key is now tenant-scoped
(STARTUP_CONFIG:${tenantId}) so multi-tenant deployments serve the
correct login page config per tenant.
The middleware is intentionally minimal — no subdomain parsing, no
OIDC claim extraction. The private fork's reverse proxy or auth
gateway sets the header.
* feat: accept optional tenantId in updateInterfacePermissions
When tenantId is provided, the function re-enters inside
tenantStorage.run({ tenantId }) so all downstream Mongoose queries
target that tenant's roles instead of the system context. This lets
the private fork's tenant provisioning flow call
updateInterfacePermissions per-tenant after creating tenant-scoped
ADMIN/USER roles.
* fix: tenant-filter $lookup in getPromptGroup aggregation
The $lookup stage in getPromptGroup() queried the prompts collection
without tenant filtering. While the outer PromptGroup aggregate is
protected by the tenantIsolation plugin's pre('aggregate') hook,
$lookup runs as an internal MongoDB operation that bypasses Mongoose
hooks entirely.
Converts from simple field-based $lookup to pipeline-based $lookup
with an explicit tenantId match when tenant context is active.
* fix: replace field-level unique indexes with tenant-scoped compounds
Field-level unique:true creates a globally-unique single-field index in
MongoDB, which would cause insert failures across tenants sharing the
same ID values.
- agent.id: removed field-level unique, added { id, tenantId } compound
- convo.conversationId: removed field-level unique (compound at line 50
already exists: { conversationId, user, tenantId })
- message.messageId: removed field-level unique (compound at line 165
already exists: { messageId, user, tenantId })
- preset.presetId: removed field-level unique, added { presetId, tenantId }
compound
* fix: scope MODELS_CONFIG, ENDPOINT_CONFIG, PLUGINS, TOOLS caches by tenant
These caches store per-tenant configuration (available models, endpoint
settings, plugin availability, tool definitions) but were using global
cache keys. In multi-tenant mode, one tenant's cached config would be
served to all tenants.
Appends :${tenantId} to cache keys when tenant context is active.
Falls back to the unscoped key when no tenant context exists (backward
compatible for single-tenant OSS deployments).
Covers all read, write, and delete sites:
- ModelController.js: get/set MODELS_CONFIG
- PluginController.js: get/set PLUGINS, get/set TOOLS
- getEndpointsConfig.js: get/set/delete ENDPOINT_CONFIG
- app.js: delete ENDPOINT_CONFIG (clearEndpointConfigCache)
- mcp.js: delete TOOLS (updateMCPTools, mergeAppTools)
- importers.js: get ENDPOINT_CONFIG
* fix: add getTenantId to PluginController spec mock
The data-schemas mock was missing getTenantId, causing all
PluginController tests to throw when the controller calls
getTenantId() for tenant-scoped cache keys.
* fix: address review findings — migration, strict-mode, DRY, types
Addresses all CRITICAL, MAJOR, and MINOR review findings:
F1 (CRITICAL): Add agents, conversations, messages, presets to
SUPERSEDED_INDEXES in tenantIndexes.ts so dropSupersededTenantIndexes()
drops the old single-field unique indexes that block multi-tenant inserts.
F2 (CRITICAL): Unknown bulkWrite op types now throw in strict mode
instead of silently passing through without tenant injection.
F3 (MAJOR): Replace wildcard export with named export for
tenantSafeBulkWrite, hiding _resetBulkWriteStrictCache from the
public package API.
F5 (MAJOR): Restore AnyBulkWriteOperation<IAclEntry>[] typing on
bulkWriteAclEntries — the unparameterized wrapper accepts parameterized
ops as a subtype.
F7 (MAJOR): Fix config.js tenant precedence — JWT-derived
req.user.tenantId now takes priority over the X-Tenant-Id header for
authenticated requests.
F8 (MINOR): Extract scopedCacheKey() helper into tenantContext.ts and
replace all 11 inline occurrences across 7 files.
F9 (MINOR): Use simple localField/foreignField $lookup for the
non-tenant getPromptGroup path (more efficient index seeks).
F12 (NIT): Remove redundant BulkOp type alias.
F13 (NIT): Remove debug log that leaked raw tenantId.
* fix: add new superseded indexes to tenantIndexes test fixture
The test creates old indexes to verify the migration drops them.
Missing fixture entries for agents.id_1, conversations.conversationId_1,
messages.messageId_1, and presets.presetId_1 caused the count assertion
to fail (expected 22, got 18).
* fix: restore logger.warn for unknown bulk op types in non-strict mode
* fix: block SYSTEM_TENANT_ID sentinel from external header input
CRITICAL: preAuthTenantMiddleware accepted any string as X-Tenant-Id,
including '__SYSTEM__'. The tenantIsolation plugin treats SYSTEM_TENANT_ID
as an explicit bypass — skipping ALL query filters. A client sending
X-Tenant-Id: __SYSTEM__ to pre-auth routes (/api/share, /api/config,
/api/auth, /oauth) would execute Mongoose operations without tenant
isolation.
Fixes:
- preAuthTenantMiddleware rejects SYSTEM_TENANT_ID in header
- scopedCacheKey returns the base key (not key:__SYSTEM__) in system
context, preventing stale cache entries during runAsSystem()
- updateInterfacePermissions guards tenantId against SYSTEM_TENANT_ID
- $lookup pipeline separates $expr join from constant tenantId match
for better index utilization
- Regression test for sentinel rejection in preAuthTenant.spec.ts
- Remove redundant getTenantId() call in config.js
* test: add missing deleteMany/replaceOne coverage, fix vacuous ALS assertions
bulkWrite spec:
- deleteMany: verifies tenant-scoped deletion leaves other tenants untouched
- replaceOne: verifies tenantId injected into both filter and replacement
- replaceOne overwrite: verifies a conflicting tenantId in the replacement
document is overwritten by the ALS tenant (defense-in-depth)
- empty ops array: verifies graceful handling
preAuthTenant spec:
- All negative-case tests now use the capturedNext pattern to verify
getTenantId() inside the middleware's execution context, not the
test runner's outer frame (which was always undefined regardless)
* feat: tenant-isolate MESSAGES cache, FLOWS cache, and GenerationJobManager
MESSAGES cache (streamAudio.js):
- Cache key now uses scopedCacheKey(messageId) to prefix with tenantId,
preventing cross-tenant message content reads during TTS streaming.
FLOWS cache (FlowStateManager):
- getFlowKey() now generates ${type}:${tenantId}:${flowId} when tenant
context is active, isolating OAuth flow state per tenant.
GenerationJobManager:
- tenantId added to SerializableJobData and GenerationJobMetadata
- createJob() captures the current ALS tenant context (excluding
SYSTEM_TENANT_ID) and stores it in job metadata
- SSE subscription endpoint validates job.metadata.tenantId matches
req.user.tenantId, blocking cross-tenant stream access
- Both InMemoryJobStore and RedisJobStore updated to accept tenantId
* fix: add getTenantId and SYSTEM_TENANT_ID to MCP OAuth test mocks
FlowStateManager.getFlowKey() now calls getTenantId() for tenant-scoped
flow keys. The 4 MCP OAuth test files mock @librechat/data-schemas
without these exports, causing TypeError at runtime.
* fix: correct import ordering per AGENTS.md conventions
Package imports sorted shortest to longest line length, local imports
sorted longest to shortest — fixes ordering violations introduced by
our new imports across 8 files.
* fix: deserialize tenantId in RedisJobStore — cross-tenant SSE guard was no-op in Redis mode
serializeJob() writes tenantId to the Redis hash via Object.entries,
but deserializeJob() manually enumerates fields and omitted tenantId.
Every getJob() from Redis returned tenantId: undefined, causing the
SSE route's cross-tenant guard to short-circuit (undefined && ... → false).
* test: SSE tenant guard, FlowStateManager key consistency, ALS scope docs
SSE stream tenant tests (streamTenant.spec.js):
- Cross-tenant user accessing another tenant's stream → 403
- Same-tenant user accessing own stream → allowed
- OSS mode (no tenantId on job) → tenant check skipped
FlowStateManager tenant tests (manager.tenant.spec.ts):
- completeFlow finds flow created under same tenant context
- completeFlow does NOT find flow under different tenant context
- Unscoped flows are separate from tenant-scoped flows
Documentation:
- JSDoc on getFlowKey documenting ALS context consistency requirement
- Comment on streamAudio.js scopedCacheKey capture site
* fix: SSE stream tests hang on success path, remove internal fork references
The success-path tests entered the SSE streaming code which never
closes, causing timeout. Mock subscribe() to end the response
immediately. Restructured assertions to verify non-403/non-404.
Removed "private fork" and "OSS" references from code and test
descriptions — replaced with "deployment layer", "multi-tenant
deployments", and "single-tenant mode".
* fix: address review findings — test rigor, tenant ID validation, docs
F1: SSE stream tests now mock subscribe() with correct signature
(streamId, writeEvent, onDone, onError) and assert 200 status,
verifying the tenant guard actually allows through same-tenant users.
F2: completeFlow logs the attempted key and ALS tenantId when flow
is not found, so reverse proxy misconfiguration (missing X-Tenant-Id
on OAuth callback) produces an actionable warning.
F3/F10: preAuthTenantMiddleware validates tenant ID format — rejects
colons, special characters, and values exceeding 128 chars. Trims
whitespace. Prevents cache key collisions via crafted headers.
F4: Documented cache invalidation scope limitation in
clearEndpointConfigCache — only the calling tenant's key is cleared;
other tenants expire via TTL.
F7: getFlowKey JSDoc now lists all 8 methods requiring consistent
ALS context.
F8: Added dedicated scopedCacheKey unit tests — base key without
context, base key in system context, scoped key with tenant, no
ALS leakage across scope boundaries.
* fix: revert flow key tenant scoping, fix SSE test timing
FlowStateManager: Reverts tenant-scoped flow keys. OAuth callbacks
arrive without tenant ALS context (provider redirects don't carry
X-Tenant-Id), so completeFlow/failFlow would never find flows
created under tenant context. Flow IDs are random UUIDs with no
collision risk, and flow data is ephemeral (TTL-bounded).
SSE tests: Use process.nextTick for onDone callback so Express
response headers are flushed before res.write/res.end are called.
* fix: restore getTenantId import for completeFlow diagnostic log
* fix: correct completeFlow warning message, add missing flow test
The warning referenced X-Tenant-Id header consistency which was only
relevant when flow keys were tenant-scoped (since reverted). Updated
to list actual causes: TTL expiry, missing flow, or routing to a
different instance without shared Keyv storage.
Removed the getTenantId() call and import — no longer needed since
flow keys are unscoped.
Added test for the !flowState branch in completeFlow — verifies
return false and logger.warn on nonexistent flow ID.
* fix: add explicit return type to recursive updateInterfacePermissions
The recursive call (tenantId branch calls itself without tenantId)
causes TypeScript to infer circular return type 'any'. Adding
explicit Promise<void> satisfies the rollup typescript plugin.
* fix: update MCPOAuthRaceCondition test to match new completeFlow warning
* fix: clearEndpointConfigCache deletes both scoped and unscoped keys
Unauthenticated /api/endpoints requests populate the unscoped
ENDPOINT_CONFIG key. Admin config mutations clear only the
tenant-scoped key, leaving the unscoped entry stale indefinitely.
Now deletes both when in tenant context.
* fix: tenant guard on abort/status endpoints, warn logs, test coverage
F1: Add tenant guard to /chat/status/:conversationId and /chat/abort
matching the existing guard on /chat/stream/:streamId. The status
endpoint exposes aggregatedContent (AI response text) which requires
tenant-level access control.
F2: preAuthTenantMiddleware now logs warn for rejected __SYSTEM__
sentinel and malformed tenant IDs, providing observability for
bypass probing attempts.
F3: Abort fallback path (getActiveJobIdsForUser) now has tenant
check after resolving the job.
F4: Test for strict mode + SYSTEM_TENANT_ID — verifies runAsSystem
bypasses tenantSafeBulkWrite without throwing in strict mode.
F5: Test for job with tenantId + user without tenantId → 403.
F10: Regex uses idiomatic hyphen-at-start form.
F11: Test descriptions changed from "rejects" to "ignores" since
middleware calls next() (not 4xx).
Also fixes MCPOAuthRaceCondition test assertion to match updated
completeFlow warning message.
* fix: test coverage for logger.warn, status/abort guards, consistency
A: preAuthTenant spec now mocks logger and asserts warn calls for
__SYSTEM__ sentinel, malformed characters, and oversized headers.
B: streamTenant spec expanded with status and abort endpoint tests —
cross-tenant status returns 403, same-tenant returns 200 with body,
cross-tenant abort returns 403.
C: Abort endpoint uses req.user.tenantId (not req.user?.tenantId)
matching stream/status pattern — requireJwtAuth guarantees req.user.
D: Malformed header warning now includes ip in log metadata,
matching the sentinel warning for consistent SOC correlation.
* fix: assert ip field in malformed header warn tests
* fix: parallelize cache deletes, document tenant guard, fix import order
- clearEndpointConfigCache uses Promise.all for independent cache
deletes instead of sequential awaits
- SSE stream tenant guard has inline comment explaining backward-compat
behavior for untenanted legacy jobs
- conversation.ts local imports reordered longest-to-shortest per
AGENTS.md
* fix: tenant-qualify userJobs keys, document tenant guard backward-compat
Job store userJobs keys now include tenantId when available:
- Redis: stream:user:{tenantId:userId}:jobs (falls back to
stream:user:{userId}:jobs when no tenant)
- InMemory: composite key tenantId:userId in userJobMap
getActiveJobIdsByUser/getActiveJobIdsForUser accept optional tenantId
parameter, threaded through from req.user.tenantId at all call sites
(/chat/active and /chat/abort fallback).
Added inline comments on all three SSE tenant guards explaining the
backward-compat design: untenanted legacy jobs remain accessible
when the userId check passes.
* fix: parallelize cache deletes, document tenant guard, fix import order
Fix InMemoryJobStore.getActiveJobIdsByUser empty-set cleanup to use
the tenant-qualified userKey instead of bare userId — prevents
orphaned empty Sets accumulating in userJobMap for multi-tenant users.
Document cross-tenant staleness in clearEndpointConfigCache JSDoc —
other tenants' scoped keys expire via TTL, not active invalidation.
* fix: cleanup userJobMap leak, startup warning, DRY tenant guard, docs
F1: InMemoryJobStore.cleanup() now removes entries from userJobMap
before calling deleteJob, preventing orphaned empty Sets from
accumulating with tenant-qualified composite keys.
F2: Startup warning when TENANT_ISOLATION_STRICT is active — reminds
operators to configure reverse proxy to control X-Tenant-Id header.
F3: mergeAppTools JSDoc documents that tenant-scoped TOOLS keys are
not actively invalidated (matching clearEndpointConfigCache pattern).
F5: Abort handler getActiveJobIdsForUser call uses req.user.tenantId
(not req.user?.tenantId) — consistent with stream/status handlers.
F6: updateInterfacePermissions JSDoc clarifies SYSTEM_TENANT_ID
behavior — falls through to caller's ALS context.
F7: Extracted hasTenantMismatch() helper, replacing three identical
inline tenant guard blocks across stream/status/abort endpoints.
F9: scopedCacheKey JSDoc documents both passthrough cases (no context
and SYSTEM_TENANT_ID context).
* fix: clean userJobMap in evictOldest — same leak as cleanup()
* feat: add MCPServerSource type, tenantMcpPolicy schema, and source-based dbSourced wiring
- Add `tenantMcpPolicy` to `mcpSettings` in YAML config schema with
`enabled`, `maxServersPerTenant`, `allowedTransports`, and `allowedDomains`
- Add `MCPServerSource` type ('yaml' | 'config' | 'user') and `source`
field to `ParsedServerConfig`
- Change `dbSourced` determination from `!!config.dbId` to
`config.source === 'user'` across MCPManager, ConnectionsRepository,
UserConnectionManager, and MCPServerInspector
- Set `source: 'user'` on all DB-sourced servers in ServerConfigsDB
* feat: three-layer MCPServersRegistry with config cache and lazy init
- Add `configCacheRepo` as third repository layer between YAML cache and
DB for admin-defined config-source MCP servers
- Implement `ensureConfigServers()` that identifies config-override servers
from resolved `getAppConfig()` mcpConfig, lazily inspects them, and
caches parsed configs with `source: 'config'`
- Add `lazyInitConfigServer()` with timeout, stub-on-failure, and
concurrent-init deduplication via `pendingConfigInits` map
- Extend `getAllServerConfigs()` with optional `configServers` param for
three-way merge: YAML → Config → User
- Add `getServerConfig()` lookup through config cache layer
- Add `invalidateConfigCache()` for clearing config-source inspection
results on admin config mutations
- Tag `source: 'yaml'` on CACHE-stored servers and `source: 'user'` on
DB-stored servers in `addServer()` and `addServerStub()`
* feat: wire tenant context into MCP controllers, services, and cache invalidation
- Resolve config-source servers via `getAppConfig({ role, tenantId })`
in `getMCPTools()` and `getMCPServersList()` controllers
- Pass `ensureConfigServers()` results through `getAllServerConfigs()`
for three-way merge of YAML + Config + User servers
- Add tenant/role context to `getMCPSetupData()` and connection status
routes via `getTenantId()` from ALS
- Add `clearMcpConfigCache()` to `invalidateConfigCaches()` so admin
config mutations trigger re-inspection of config-source MCP servers
* feat: enforce tenantMcpPolicy on admin config mcpServers mutations
- Add `validateMcpServerPolicy()` helper that checks mcpServers against
operator-defined `tenantMcpPolicy` (enabled, maxServersPerTenant,
allowedTransports, allowedDomains)
- Wire validation into `upsertConfigOverrides` and `patchConfigField`
handlers — rejects with 403 when policy is violated
- Infer transport type from config shape (command → stdio, url protocol
→ websocket/sse, type field → streamable-http)
- Validate server domains against policy allowlist when configured
* revert: remove tenantMcpPolicy schema and enforcement
The existing admin config CRUD routes already provide the mechanism
for granular MCP server prepopulation (groups, roles, users). The
tenantMcpPolicy gating adds unnecessary complexity that can be
revisited if needed in the future.
- Remove tenantMcpPolicy from mcpSettings Zod schema
- Remove validateMcpServerPolicy helper and TenantMcpPolicy interface
- Remove policy enforcement from upsertConfigOverrides and
patchConfigField handlers
* test: update test assertions for source field and config-server wiring
- Use objectContaining in MCPServersRegistry reset test to account for
new source: 'yaml' field on CACHE-stored configs
- Add getTenantId and ensureConfigServers mocks to MCP route tests
- Add getAppConfig mock to route test Config service mock
- Update getMCPSetupData assertion to expect second options argument
- Update getAllServerConfigs assertions for new configServers parameter
* fix: disconnect active connections when config-source servers are evicted
When admin config overrides change and config-source MCP servers are
removed, the invalidation now proactively disconnects active connections
for evicted servers instead of leaving them lingering until timeout.
- Return evicted server names from invalidateConfigCache()
- Disconnect app-level connections for evicted servers in
clearMcpConfigCache() via MCPManager.appConnections.disconnect()
* fix: address code review findings (CRITICAL, MAJOR, MINOR)
CRITICAL fixes:
- Scope configCacheRepo keys by config content hash to prevent
cross-tenant cache poisoning when two tenants define the same
server name with different configurations
- Change dbSourced checks from `source === 'user'` to
`source !== 'yaml' && source !== 'config'` so undefined source
(pre-upgrade cached configs) fails closed to restricted mode
MAJOR fixes:
- Derive OAuth servers from already-computed mcpConfig instead of
calling getOAuthServers() separately — config-source OAuth servers
are now properly detected
- Add parseInt radix (10) and NaN guard with fallback to 30_000
for CONFIG_SERVER_INIT_TIMEOUT_MS
- Add CONFIG_CACHE_NAMESPACE to aggregate-key branch in
ServerConfigsCacheFactory to avoid SCAN-based Redis stalls
- Remove `if (role || tenantId)` guard in getMCPSetupData — config
servers now always resolve regardless of tenant context
MINOR fixes:
- Extract resolveAllMcpConfigs() helper in mcp controller to
eliminate 3x copy-pasted config resolution boilerplate
- Distinguish "not initialized" from real errors in
clearMcpConfigCache — log actual failures instead of swallowing
- Remove narrative inline comments per style guide
- Remove dead try/catch inside Promise.allSettled in
ensureConfigServers (inner method never throws)
- Memoize YAML server names to avoid repeated cacheConfigsRepo.getAll()
calls per request
Test updates:
- Add ensureConfigServers mock to registry test fixtures
- Update getMCPSetupData assertions for inline OAuth derivation
* fix: address code review findings (CRITICAL, MAJOR, MINOR)
CRITICAL fixes:
- Break circular dependency: move CONFIG_CACHE_NAMESPACE from
MCPServersRegistry to ServerConfigsCacheFactory
- Fix dbSourced fail-closed: use source field when present, fall back to
legacy dbId check when absent (backward-compatible with pre-upgrade
cached configs that lack source field)
MAJOR fixes:
- Add CONFIG_CACHE_NAMESPACE to aggregate-key set in
ServerConfigsCacheFactory to avoid SCAN-based Redis stalls
- Add comprehensive test suite (ensureConfigServers.test.ts, 18 tests)
covering lazy init, stub-on-failure, cross-tenant isolation via config
hash keys, concurrent deduplication, merge order, and cache invalidation
MINOR fixes:
- Update MCPServerInspector test assertion for dbSourced change
* fix: restore getServerConfig lookup for config-source servers (NEW-1)
Add configNameToKey map that indexes server name → hash-based cache key
for O(1) lookup by name in getServerConfig. This restores the config
cache layer that was dropped when hash-based keys were introduced.
Without this fix, config-source servers appeared in tool listings
(via getAllServerConfigs) but getServerConfig returned undefined,
breaking all connection and tool call paths.
- Populate configNameToKey in ensureSingleConfigServer
- Clear configNameToKey in invalidateConfigCache and reset
- Clear stale read-through cache entries after lazy init
- Remove dead code in invalidateConfigCache (config.title, key parsing)
- Add getServerConfig tests for config-source server lookup
* fix: eliminate configNameToKey race via caller-provided configServers param
Replace the process-global configNameToKey map (last-writer-wins under
concurrent multi-tenant load) with a configServers parameter on
getServerConfig. Callers pass the pre-resolved config servers map
directly — no shared mutable state, no cross-tenant race.
- Add optional configServers param to getServerConfig; when provided,
returns matching config directly without any global lookup
- Remove configNameToKey map entirely (was the source of the race)
- Extract server names from cache keys via lastIndexOf in
invalidateConfigCache (safe for names containing colons)
- Use mcpConfig[serverName] directly in getMCPTools instead of a
redundant getServerConfig call
- Add cross-tenant isolation test for getServerConfig
* fix: populate read-through cache after config server lazy init
After lazyInitConfigServer succeeds, write the parsed config to
readThroughCache keyed by serverName so that getServerConfig calls
from ConnectionsRepository, UserConnectionManager, and
MCPManager.callTool find the config without needing configServers.
Without this, config-source servers appeared in tool listings but
every connection attempt and tool call returned undefined.
* fix: user-scoped getServerConfig fallback to server-only cache key
When getServerConfig is called with a userId (e.g., from callTool or
UserConnectionManager), the cache key is serverName::userId. Config-source
servers are cached under the server-only key (no userId). Add a fallback
so user-scoped lookups find config-source servers in the read-through cache.
* fix: configCacheRepo fallback, isUserSourced DRY, cross-process race
CRITICAL: Add findInConfigCache fallback in getServerConfig so
config-source servers remain reachable after readThroughCache TTL
expires (5s). Without this, every tool call after 5s returned
undefined for config-source servers.
MAJOR: Extract isUserSourced() helper to mcp/utils.ts and replace
all 5 inline dbSourced ternary expressions (MCPManager x2,
ConnectionsRepository, UserConnectionManager, MCPServerInspector).
MAJOR: Fix cross-process Redis race in lazyInitConfigServer — when
configCacheRepo.add throws (key exists from another process), fall
back to reading the existing entry instead of returning undefined.
MINOR: Parallelize invalidateConfigCache awaits with Promise.all.
Remove redundant .catch(() => {}) inside Promise.allSettled.
Tighten dedup test assertion to toBe(1).
Add TTL-expiry tests for getServerConfig (with and without userId).
* feat: thread configServers through getAppToolFunctions and formatInstructionsForContext
Add optional configServers parameter to getAppToolFunctions,
getInstructions, and formatInstructionsForContext so config-source
server tools and instructions are visible to agent initialization
and context injection paths.
Existing callers (boot-time init, tests) pass no argument and
continue to work unchanged. Agent runtime paths can now thread
resolved config servers from request context.
* fix: stale failure stubs retry after 5 min, upsert for cross-process races
- Add CONFIG_STUB_RETRY_MS (5 min) — stale failure stubs are retried
instead of permanently disabling config-source servers after transient
errors (DNS outage, cold-start race)
- Extract upsertConfigCache() helper that tries add then falls back to
update, preventing cross-process Redis races where a second instance's
successful inspection result was discarded
- Add test for stale-stub retry after CONFIG_STUB_RETRY_MS
* fix: stamp updatedAt on failure stubs, null-guard callTool config, test cleanup
- Add updatedAt: Date.now() to failure stubs in lazyInitConfigServer so
CONFIG_STUB_RETRY_MS (5 min) window works correctly — without it, stubs
were always considered stale (updatedAt ?? 0 → epoch → always expired)
- Add null guard for rawConfig in MCPManager.callTool before passing to
preProcessGraphTokens — prevents unsafe `as` cast on undefined
- Log double-failure in upsertConfigCache instead of silently swallowing
- Replace module-scope Date.now monkey-patch with jest.useFakeTimers /
jest.setSystemTime / jest.useRealTimers in ensureConfigServers tests
* fix: server-only readThrough fallback only returns truthy values
Prevents a cached undefined from a prior no-userId lookup from
short-circuiting the DB query on a subsequent userId-scoped lookup.
* fix: remove findInConfigCache to eliminate cross-tenant config leakage
The findInConfigCache prefix scan (serverName:*) could return any
tenant's config after readThrough TTL expires, violating tenant
isolation. Config-source servers are now ONLY resolvable through:
1. The configServers param (callers with tenant context from ALS)
2. The readThrough cache (populated by ensureSingleConfigServer,
5s TTL, repopulated on every HTTP request via resolveAllMcpConfigs)
Connection/tool-call paths without tenant context rely exclusively on
the readThrough cache. If it expires before the next HTTP request
repopulates it, the server is not found — which is correct because
there is no tenant context to determine which config to return.
- Remove findInConfigCache method and its call in getServerConfig
- Update server-only readThrough fallback to only return truthy values
(prevents cached undefined from short-circuiting user-scoped DB lookup)
- Update tests to document tenant isolation behavior after cache expiry
* style: fix import order per AGENTS.md conventions
Sort package imports shortest-to-longest, local imports longest-to-shortest
across MCPServersRegistry, ConnectionsRepository, MCPManager,
UserConnectionManager, and MCPServerInspector.
* fix: eliminate cross-tenant readThrough contamination and TTL-expiry tool failures
Thread pre-resolved serverConfig from tool creation context into
callTool, removing dependency on the readThrough cache for config-source
servers. This fixes two issues:
- Cross-tenant contamination: the readThrough cache key was unscoped
(just serverName), so concurrent multi-tenant requests for same-named
servers would overwrite each other's entries
- TTL expiry: tool calls happening >5s after config resolution would
fail with "Configuration not found" because the readThrough entry
had expired
Changes:
- Add optional serverConfig param to MCPManager.callTool — uses
provided config directly, falling back to getServerConfig lookup
for YAML/user servers
- Thread serverConfig from createMCPTool through createToolInstance
closure to callTool
- Remove readThrough write from ensureSingleConfigServer — config-source
servers are only accessible via configServers param (tenant-scoped)
- Remove server-only readThrough fallback from getServerConfig
- Increase config cache hash from 8 to 16 hex chars (64-bit)
- Add isUserSourced boundary tests for all source/dbId combinations
- Fix double Object.keys call in getMCPTools controller
- Update test assertions for new getServerConfig behavior
* fix: cache base configs for config-server users; narrow upsertConfigCache error handling
- Refactor getAllServerConfigs to separate base config fetch (YAML + DB)
from config-server layering. Base configs are cached via readThroughCacheAll
regardless of whether configServers is provided, eliminating uncached
MongoDB queries per request for config-server users
- Narrow upsertConfigCache catch to duplicate-key errors only;
infrastructure errors (Redis timeouts, network failures) now propagate
instead of being silently swallowed, preventing inspection storms
during outages
* fix: restore correct merge order and document upsert error matching
- Restore YAML → Config → User DB precedence in getAllServerConfigs
(user DB servers have highest precedence, matching the JSDoc contract)
- Add source comment on upsertConfigCache duplicate-key detection
linking to the two cache implementations that define the error message
* feat: complete config-source server support across all execution paths
Wire configServers through the entire agent execution pipeline so
config-source MCP servers are fully functional — not just visible in
listings but executable in agent sessions.
- Thread configServers into handleTools.js agent tool pipeline: resolve
config servers from tenant context before MCP tool iteration, pass to
getServerConfig, createMCPTools, and createMCPTool
- Thread configServers into agent instructions pipeline:
applyContextToAgent → getMCPInstructionsForServers →
formatInstructionsForContext, resolved in client.js before agent
context application
- Add configServers param to createMCPTool and createMCPTools for
reconnect path fallback
- Add source field to redactServerSecrets allowlist for client UI
differentiation of server tiers
- Narrow invalidateConfigCache to only clear readThroughCacheAll (merged
results), preserving YAML individual-server readThrough entries
- Update context.spec.ts assertions for new configServers parameter
* fix: add missing mocks for config-source server dependencies in client.test.js
Mock getMCPServersRegistry, getAppConfig, and getTenantId that were added
to client.js but not reflected in the test file's jest.mock declarations.
* fix: update formatInstructionsForContext assertions for configServers param
The test assertions expected formatInstructionsForContext to be called with
only the server names array, but it now receives configServers as a second
argument after the config-source server feature wiring.
* fix: move configServers resolution before MCP tool loop to avoid TDZ
configServers was declared with `let` after the first tool loop but
referenced inside it via getServerConfig(), causing a ReferenceError
temporal dead zone. Move declaration and resolution before the loop,
using tools.some(mcpToolPattern) to gate the async resolution.
* fix: address review findings — cache bypass, discoverServerTools gap, DRY
- #2: getAllServerConfigs now always uses getBaseServerConfigs (cached via
readThroughCacheAll) instead of bypassing it when configServers is present.
Extracts user-DB entries from cached base by diffing against YAML keys
to maintain YAML → Config → User DB merge order without extra MongoDB calls.
- #3: Add configServers param to ToolDiscoveryOptions and thread it through
discoverServerTools → getServerConfig so config-source servers are
discoverable during OAuth reconnection flows.
- #6: Replace inline import() type annotations in context.ts with proper
import type { ParsedServerConfig } per AGENTS.md conventions.
- #7: Extract resolveConfigServers(req) helper in MCP.js and use it from
handleTools.js and client.js, eliminating the duplicated 6-line config
resolution pattern.
- #10: Restore removed "why" comment explaining getLoaded() vs getAll()
choice in getMCPSetupData — documents non-obvious correctness constraint.
- #11: Fix incomplete JSDoc param type on resolveAllMcpConfigs.
* fix: consolidate imports, reorder constants, fix YAML-DB merge edge case
- Merge duplicate @librechat/data-schemas requires in MCP.js into one
- Move resolveConfigServers after module-level constants
- Fix getAllServerConfigs edge case where user-DB entry overriding a
YAML entry with the same name was excluded from userDbConfigs; now
uses reference equality check to detect DB-overwritten YAML keys
* fix: replace fragile string-match error detection with proper upsert method
Add upsert() to IServerConfigsRepositoryInterface and all implementations
(InMemory, Redis, RedisAggregateKey, DB). This eliminates the brittle
error message string match ('already exists in cache') in upsertConfigCache
that was the only thing preventing cross-process init races from silently
discarding inspection results.
Each implementation handles add-or-update atomically:
- InMemory: direct Map.set()
- Redis: direct cache.set()
- RedisAggregateKey: read-modify-write under write lock
- DB: delegates to update() (DB servers use explicit add() with ACL setup)
* fix: wire configServers through remaining HTTP endpoints
- getMCPServerById: use resolveAllMcpConfigs instead of bare getServerConfig
- reinitialize route: resolve configServers before getServerConfig
- auth-values route: resolve configServers before getServerConfig
- getOAuthHeaders: accept configServers param, thread from callers
- Update mcp.spec.js tests to mock getAllServerConfigs for GET by name
* fix: thread serverConfig through getConnection for config-source servers
Config-source servers exist only in configCacheRepo, not in YAML cache or
DB. When callTool → getConnection → getUserConnection → getServerConfig
runs without configServers, it returns undefined and throws. Fix by
threading the pre-resolved serverConfig (providedConfig) from callTool
through getConnection → getUserConnection → createUserConnectionInternal,
using it as a fallback before the registry lookup.
* fix: thread configServers through reinit, reconnect, and tool definition paths
Wire configServers through every remaining call chain that creates or
reconnects MCP server connections:
- reinitMCPServer: accepts serverConfig and configServers, uses them for
getServerConfig fallback, getConnection, and discoverServerTools
- reconnectServer: accepts and passes configServers to reinitMCPServer
- createMCPTools/createMCPTool: pass configServers to reconnectServer
- ToolService.loadToolDefinitionsWrapper: resolves configServers from req,
passes to both reinitMCPServer call sites
- reinitialize route: passes serverConfig and configServers to reinitMCPServer
* fix: address review findings — simplify merge, harden error paths, fix log labels
- Simplify getAllServerConfigs merge: replace fragile reference-equality
loop with direct spread { ...yamlConfigs, ...configServers, ...base }
- Guard upsertConfigCache in lazyInitConfigServer catch block so cache
failures don't mask the original inspection error
- Deduplicate getYamlServerNames cold-start with promise dedup pattern
- Remove dead `if (!mcpConfig)` guard in getMCPSetupData
- Fix hardcoded "App server" in ServerConfigsCacheRedisAggregateKey error
messages — now uses this.namespace for correct Config/App labeling
- Remove misleading OAuth callback comment about readThrough cache
- Move resolveConfigServers after module-level constants in MCP.js
* fix: clear rejected yamlServerNames promise, fix config-source reinspect, fix reset log label
- Clear yamlServerNamesPromise on rejection so transient cache errors
don't permanently prevent ensureConfigServers from working
- Skip reinspectServer for config-source servers (source: 'config') in
reinitMCPServer — they lack a CACHE/DB storage location; retry is
handled by CONFIG_STUB_RETRY_MS in ensureConfigServers
- Use source field instead of dbId for storageLocation derivation
- Fix remaining hardcoded "App" in reset() leaderCheck message
* fix: persist oauthHeaders in flow state for config-source OAuth servers
The OAuth callback route has no JWT auth context and cannot resolve
config-source server configs. Previously, getOAuthHeaders would silently
return {} for config-source servers, dropping custom token exchange headers.
Now oauthHeaders are persisted in MCPOAuthFlowMetadata during flow
initiation (which has auth context), and the callback reads them from
the stored flow state with a fallback to the registry lookup for
YAML/user-DB servers.
* fix: update tests for getMCPSetupData null guard removal and ToolService mock
- MCP.spec.js: update test to expect graceful handling of null mcpConfig
instead of a throw (getAllServerConfigs always returns an object)
- MCP.js: add defensive || {} for Object.entries(mcpConfig) in case of
null from test mocks
- ToolService.spec.js: add missing mock for ~/server/services/MCP
(resolveConfigServers)
* fix: address review findings — DRY, naming, logging, dead code, defensive guards
- #1: Simplify getAllServerConfigs to single getBaseServerConfigs call,
eliminating redundant double-fetch of cacheConfigsRepo.getAll()
- #2: Add warning log when oauthHeaders absent from OAuth callback flow state
- #3: Extract resolveAllMcpConfigs to MCP.js service layer; controller
imports shared helper instead of reimplementing
- #4: Rename _serverConfig/_provider to capturedServerConfig/capturedProvider
in createToolInstance — these are actively used, not unused
- #5: Log rejected results from ensureConfigServers Promise.allSettled
so cache errors are visible instead of silently dropped
- #6: Remove dead 'MCP config not found' error handlers from routes
- #7: Document circular-dependency reason for dynamic require in clearMcpConfigCache
- #8: Remove logger.error from withTimeout to prevent double-logging timeouts
- #10: Add explicit userId guard in ServerConfigsDB.upsert with clear error message
- #12: Use spread instead of mutation in addServer for immutability consistency
- Add upsert mock to ensureConfigServers.test.ts DB mock
- Update route tests for resolveAllMcpConfigs import change
* fix: restore correct merge priority, use immutable spread, fix test mock
- getAllServerConfigs: { ...configServers, ...base } so userDB wins over
configServers, matching documented "User DB (highest)" priority
- lazyInitConfigServer: use immutable spread instead of direct mutation
for parsedConfig.source, consistent with addServer fix
- Fix test to mock getAllServerConfigs as {} instead of null, remove
unnecessary || {} defensive guard in getMCPSetupData
* fix: error handling, stable hashing, flatten nesting, remove dead param
- Wrap resolveConfigServers/resolveAllMcpConfigs in try/catch with
graceful {} fallback so transient DB/cache errors don't crash tool pipeline
- Sort keys in configCacheKey JSON.stringify for deterministic hashing
regardless of object property insertion order
- Flatten clearMcpConfigCache from 3 nested try-catch to early returns;
document that user connections are cleaned up lazily (accepted tradeoff)
- Remove dead configServers param from getAppToolFunctions (never passed)
- Add security rationale comment for source field in redactServerSecrets
* fix: use recursive key-sorting replacer in configCacheKey to prevent cross-tenant cache collision
The array replacer in JSON.stringify acts as a property allowlist at
every nesting depth, silently dropping nested keys like headers['X-API-Key'],
oauth.client_secret, etc. Two configs with different nested values but
identical top-level structure produced the same hash, causing cross-tenant
cache hits and potential credential contamination.
Switch to a function replacer that recursively sorts keys at all depths
without dropping any properties.
Also document the known gap in getOAuthServers: config-source OAuth
servers are not covered by auto-reconnection or uninstall cleanup
because callers lack request context.
* fix: move clearMcpConfigCache to packages/api to eliminate circular dependency
The function only depends on MCPServersRegistry and MCPManager, both of
which live in packages/api. Import it directly from @librechat/api in
the CJS layer instead of using dynamic require('~/config').
* chore: imports/fields ordering
* fix: address review findings — error handling, targeted lookup, test gaps
- Narrow resolveAllMcpConfigs catch to only wrap ensureConfigServers so
getAppConfig/getAllServerConfigs failures propagate instead of masking
infrastructure errors as empty server lists.
- Use targeted getServerConfig in getMCPServerById instead of fetching
all server configs for a single-server lookup.
- Forward configServers to inner createMCPTool calls so reconnect path
works for config-source servers.
- Update getAllServerConfigs JSDoc to document disjoint-key design.
- Add OAuth callback oauthHeaders fallback tests (flow state present
vs registry fallback).
- Add resolveConfigServers/resolveAllMcpConfigs unit tests covering
happy path and error propagation.
* fix: add getOAuthReconnectionManager mock to OAuth callback tests
* chore: imports ordering
* feat: add resolveAppConfigForUser utility for tenant-scoped auth config
TypeScript utility in packages/api that wraps getAppConfig in
tenantStorage.run() when the user has a tenantId, falling back to
baseOnly for new users or non-tenant deployments. Uses DI pattern
(getAppConfig passed as parameter) for testability.
Auth flows apply role-level overrides only (userId not passed)
because user/group principal resolution is deferred to post-auth.
* feat: tenant-scoped app config in auth login flows
All auth strategies (LDAP, SAML, OpenID, social login) now use a
two-phase domain check consistent with requestPasswordReset:
1. Fast-fail with base config (memory-cached, zero DB queries)
2. DB user lookup
3. Tenant-scoped re-check via resolveAppConfigForUser (only when
user has a tenantId; otherwise reuse base config)
This preserves the original fast-fail protection against globally
blocked domains while enabling tenant-specific config overrides.
OpenID error ordering preserved: AUTH_FAILED checked before domain
re-check so users with wrong providers get the correct error type.
registerUser unchanged (baseOnly, no user identity yet).
* test: add tenant-scoped config tests for auth strategies
Add resolveAppConfig.spec.ts in packages/api with 8 tests:
- baseOnly fallback for null/undefined/no-tenant users
- tenant-scoped config with role and tenantId
- ALS context propagation verified inside getAppConfig callback
- undefined role with tenantId edge case
Update strategy and AuthService tests to mock resolveAppConfigForUser
via @librechat/api. Tests verify two-phase domain check behavior:
fast-fail before DB, tenant re-check after. Non-tenant users reuse
base config without calling resolveAppConfigForUser.
* refactor: skip redundant domain re-check for non-tenant users
Guard the second isEmailDomainAllowed call with appConfig !== baseConfig
in SAML, OpenID, and social strategies. For non-tenant users the tenant
config is the same base config object, so the second check is a no-op.
Narrow eslint-disable in resolveAppConfig.spec.ts to the specific
require line instead of blanket file-level suppression.
* fix: address review findings — consistency, tests, and ordering
- Consolidate duplicate require('@librechat/api') in AuthService.js
- Add two-phase domain check to LDAP (base fast-fail before findUser),
making all strategies consistent with PR description
- Add appConfig !== baseConfig guard to requestPasswordReset second
domain check, consistent with SAML/OpenID/social strategies
- Move SAML provider check before tenant config resolution to avoid
unnecessary resolveAppConfigForUser call for wrong-provider users
- Add tenant domain rejection tests to SAML, OpenID, and social specs
verifying that tenant config restrictions actually block login
- Add error propagation tests to resolveAppConfig.spec.ts
- Remove redundant mockTenantStorage alias in resolveAppConfig.spec.ts
- Narrow eslint-disable to specific require line
* test: add tenant domain rejection test for LDAP strategy
Covers the appConfig !== baseConfig && !isEmailDomainAllowed path,
consistent with SAML, OpenID, and social strategy specs.
* refactor: rename resolveAppConfig to app/resolve per AGENTS.md
Rename resolveAppConfig.ts → resolve.ts and
resolveAppConfig.spec.ts → resolve.spec.ts to align with
the project's concise naming convention.
* fix: remove fragile reference-equality guard, add logging and docs
Remove appConfig !== baseConfig guard from all strategies and
requestPasswordReset. The guard relied on implicit cache-backend
identity semantics (in-memory Keyv returns same object reference)
that would silently break with Redis or cloned configs. The second
isEmailDomainAllowed call is a cheap synchronous check — always
running it is clearer and eliminates the coupling.
Add audit logging to requestPasswordReset domain blocks (base and
tenant), consistent with all auth strategies.
Extract duplicated error construction into makeDomainDeniedError().
Wrap resolveAppConfigForUser in requestPasswordReset with try/catch
to prevent DB errors from leaking to the client via the controller's
generic catch handler.
Document the dual tenantId propagation (ALS for DB isolation,
explicit param for cache key) in resolveAppConfigForUser JSDoc.
Add comment documenting the LDAP error-type ordering change
(cross-provider users from blocked domains now get 'domain not
allowed' instead of AUTH_FAILED).
Assert resolveAppConfigForUser is not called on LDAP provider
mismatch path.
* fix: return generic response for tenant domain block in password reset
Tenant-scoped domain rejection in requestPasswordReset now returns the
same generic "If an account with that email exists..." response instead
of an Error. This prevents user-enumeration: an attacker cannot
distinguish between "email not found" and "tenant blocks this domain"
by comparing HTTP responses.
The base-config fast-fail (pre-user-lookup) still returns an Error
since it fires before any user existence is revealed.
* docs: document phase 1 vs phase 2 domain check behavior in JSDoc
Phase 1 (base config, pre-findUser) intentionally returns Error/400
to reveal globally blocked domains without confirming user existence.
Phase 2 (tenant config, post-findUser) returns generic 200 to prevent
user-enumeration. This distinction is now explicit in the JSDoc.
* feat: add createRole and deleteRole methods to role
* feat: add admin roles handler factory and Express routes
* fix: address convention violations in admin roles handlers
* fix: rename createRole/deleteRole to avoid AccessRole name collision
The existing accessRole.ts already exports createRole/deleteRole for the
AccessRole model. In createMethods index.ts, these are spread after
roleMethods, overwriting them. Renamed our Role methods to
createRoleByName/deleteRoleByName to match the existing pattern
(getRoleByName, updateRoleByName) and avoid the collision.
* feat: add description field to Role model
- Add description to IRole, CreateRoleRequest, UpdateRoleRequest types
- Add description field to Mongoose roleSchema (default: '')
- Wire description through createRoleHandler and updateRoleHandler
- Include description in listRoles select clause so it appears in list
* fix: address Copilot review findings in admin roles handlers
* test: add unit tests for admin roles and groups handlers
* test: add data-layer tests for createRoleByName, deleteRoleByName, listUsersByRole
* fix: allow system role updates when name is unchanged
The updateRoleHandler guard rejected any request where body.name matched
a system role, even when the name was not being changed. This blocked
editing a system role's description. Compare against the URL param to
only reject actual renames to reserved names.
* fix: address external review findings for admin roles
- Block renaming system roles (ADMIN/USER) and add user migration on rename
- Add input validation: name max-length, trim on update, duplicate name check
- Replace fragile String.includes error matching with prefix-based classification
- Catch MongoDB 11000 duplicate key in createRoleByName
- Add pagination (limit/offset/total) to getRoleMembersHandler
- Reverse delete order in deleteRoleByName — reassign users before deletion
- Add role existence check in removeRoleMember; drop unused createdAt select
- Add Array.isArray guard for permissions input; use consistent ?? coalescing
- Fix import ordering per AGENTS.md conventions
- Type-cast mongoose.models.User as Model<IUser> for proper TS inference
- Add comprehensive tests: rename guards, pagination, validation, 500 paths
* fix: address re-review findings for admin roles
- Gate deleteRoleByName on existence check — skip user reassignment and
cache invalidation when role doesn't exist (fixes test mismatch)
- Reverse rename order: migrate users before renaming role so a migration
failure leaves the system in a consistent state
- Add .sort({ _id: 1 }) to listUsersByRole for deterministic pagination
- Import shared AdminMember type from data-schemas instead of local copy;
make joinedAt optional since neither groups nor roles populate it
- Change IRole.description from optional to required to match schema default
- Add data-layer tests for updateUsersByRole and countUsersByRole
- Add handler test verifying users-first rename ordering and migration
failure safety
* fix: add rollback on rename failure and update PR description
- Roll back user migration if updateRoleByName returns null during a
rename (race: role deleted between existence check and update)
- Add test verifying rollback calls updateUsersByRole in reverse
- Update PR #12400 description to reflect current test counts (56
handler tests, 40 data-layer tests) and safety features
* fix: rollback on rename throw, description validation, delete/DRY cleanup
- Hoist isRename/trimmedName above try block so catch can roll back user
migration when updateRoleByName throws (not just returns null)
- Add description type + max-length (2000) validation in create and update,
consistent with groups handler
- Remove redundant getRoleByName existence check in deleteRoleHandler —
use deleteRoleByName return value directly
- Skip no-op name write when body.name equals current name (use isRename)
- Extract getUserModel() accessor to DRY repeated Model<IUser> casts
- Use name.trim() consistently in createRoleByName error messages
- Add tests: rename-throw rollback, description validation (create+update),
update delete test mocks to match simplified handler
* fix: guard spurious rollback, harden createRole error path, validate before DB calls
- Add migrationRan flag to prevent rollback of user migration that never ran
- Return generic message on 500 in createRoleHandler, specific only for 409
- Move description validation before DB queries in updateRoleHandler
- Return existing role early when update body has no changes
- Wrap cache.set in createRoleByName with try/catch to prevent masking DB success
- Add JSDoc on 11000 catch explaining compound unique index
- Add tests: spurious rollback guard, empty update body, description validation
ordering, listUsersByRole pagination
* fix: validate permissions in create, RoleConflictError, rollback safety, cache consistency
- Add permissions type/array validation in createRoleHandler
- Introduce RoleConflictError class replacing fragile string-prefix matching
- Wrap rollback in !role null path with try/catch for correct 404 response
- Wrap deleteRoleByName cache.set in try/catch matching createRoleByName
- Narrow updateRoleHandler body type to { name?, description? }
- Add tests: non-string description in create, rollback failure logging,
permissions array rejection, description max-length assertion fix
* feat: prevent removing the last admin user
Add guard in removeRoleMember that checks countUsersByRole before
demoting an ADMIN user, returning 400 if they are the last one.
* fix: move interleaved export below imports, add await to countUsersByRole
* fix: paginate listRoles, null-guard permissions handler, fix export ordering
- Add limit/offset/total pagination to listRoles matching the groups pattern
- Add countRoles data-layer method
- Omit permissions from listRoles select (getRole returns full document)
- Null-guard re-fetched role in updateRolePermissionsHandler
- Move interleaved export below all imports in methods/index.ts
* fix: address review findings — race safety, validation DRY, type accuracy, test coverage
- Add post-write admin count verification in removeRoleMember to prevent
zero-admin race condition (TOCTOU → rollback if count hits 0)
- Make IRole.description optional; backfill in initializeRoles for
pre-existing roles that lack the field (.lean() bypasses defaults)
- Extract parsePagination, validateNameParam, validateRoleName, and
validateDescription helpers to eliminate duplicated validation
- Add validateNameParam guard to all 7 handlers reading req.params.name
- Catch 11000 in updateRoleByName and surface as 409 via RoleConflictError
- Add idempotent skip in addRoleMember when user already has target role
- Verify updateRolePermissions test asserts response body
- Add data-layer tests: listRoles sort/pagination/projection, countRoles,
and createRoleByName 11000 duplicate key race
* fix: defensive rollback in removeRoleMember, type/style cleanup, test coverage
- Wrap removeRoleMember post-write admin rollback in try/catch so a
transient DB failure cannot leave the system with zero administrators
- Replace double `as unknown[] as IRole[]` cast with `.lean<IRole[]>()`
- Type parsePagination param explicitly; extract DEFAULT/MAX page constants
- Preserve original error cause in updateRoleByName re-throw
- Add test for rollback failure path in removeRoleMember (returns 400)
- Add test for pre-existing roles missing description field (.lean())
* chore: bump @librechat/data-schemas to 0.0.47
* fix: stale cache on rename, extract renameRole helper, shared pagination, cleanup
- Fix updateRoleByName cache bug: invalidate old key and populate new key
when updates.name differs from roleName (prevents stale cache after rename)
- Extract renameRole helper to eliminate mutable outer-scope state flags
(isRename, trimmedName, migrationRan) in updateRoleHandler
- Unify system-role protection to 403 for both rename-from and rename-to
- Extract parsePagination to shared admin/pagination.ts; use in both
roles.ts and groups.ts
- Extract name.trim() to local const in createRoleByName (was called 5×)
- Remove redundant findOne pre-check in deleteRoleByName
- Replace getUserModel closure with local const declarations
- Remove redundant description ?? '' in createRoleHandler (schema default)
- Add doc comment on updateRolePermissionsHandler noting cache dependency
- Add data-layer tests for cache rename behavior (old key null, new key set)
* fix: harden role guards, add User.role index, validate names, improve tests
- Add index on User.role field for efficient member queries at scale
- Replace fragile SystemRoles key lookup with value-based Set check (6 sites)
- Elevate rename rollback failure logging to CRITICAL (matches removeRoleMember)
- Guard removeRoleMember against non-ADMIN system roles (403 for USER)
- Fix parsePagination limit=0 gotcha: use parseInt + NaN check instead of ||
- Add control character and reserved path segment validation to role names
- Simplify validateRoleName: remove redundant casts and dead conditions
- Add JSDoc to deleteRoleByName documenting non-atomic window
- Split mixed value+type import in methods/index.ts per AGENTS.md
- Add 9 new tests: permissions assertion, combined rename+desc, createRole
with permissions, pagination edge cases, control char/reserved name
rejection, system role removeRoleMember guard
* fix: exact-case reserved name check, consistent validation, cleaner createRole
- Remove .toLowerCase() from reserved name check so only exact matches
(members, permissions) are rejected, not legitimate names like "Members"
- Extract trimmed const in validateRoleName for consistent validation
- Add control char check to validateNameParam for parity with body validation
- Build createRole roleData conditionally to avoid passing description: undefined
- Expand deleteRoleByName JSDoc documenting self-healing design and no-op trade-off
* fix: scope rename rollback to only migrated users, prevent cross-role corruption
Capture user IDs before forward migration so the rollback path only
reverts users this request actually moved. Previously the rollback called
updateUsersByRole(newName, currentName) which would sweep all users with
the new role — including any independently assigned by a concurrent admin
request — causing silent cross-role data corruption.
Adds findUserIdsByRole and updateUsersRoleByIds to the data layer.
Extracts rollbackMigratedUsers helper to deduplicate rollback sites.
* fix: guard last admin in addRoleMember to prevent zero-admin lockout
Since each user has exactly one role, addRoleMember implicitly removes
the user from their current role. Without a guard, reassigning the sole
admin to a non-admin role leaves zero admins and locks out admin
management. Adds the same countUsersByRole check used in removeRoleMember.
* fix: wire findUserIdsByRole and updateUsersRoleByIds into roles route
The scoped rollback deps added in c89b5db were missing from the route
DI wiring, causing renameRole to call undefined and return a 500.
* fix: post-write admin guard in addRoleMember, compound role index, review cleanup
- Add post-write admin count check + rollback to addRoleMember to match
removeRoleMember's two-phase TOCTOU protection (prevents zero-admin via
concurrent requests)
- Replace single-field User.role index with compound { role: 1, tenantId: 1 }
to align with existing multi-tenant index pattern (email, OAuth IDs)
- Narrow listRoles dep return type to RoleListItem (projected fields only)
- Refactor validateDescription to early-return style per AGENTS.md
- Remove redundant double .lean() in updateRoleByName
- Document rename snapshot race window in renameRole JSDoc
- Document cache null-set behavior in deleteRoleByName
- Add routing-coupling comment on RESERVED_ROLE_NAMES
- Add test for addRoleMember post-write rollback
* fix: review cleanup — system-role guard, type safety, JSDoc accuracy, tests
- Add system-role guard to addRoleMember: block direct assignment to
non-ADMIN system roles (403), symmetric with removeRoleMember
- Fix RESERVED_ROLE_NAMES comment: explain semantic URL ambiguity, not
a routing conflict (Express resolves single vs multi-segment correctly)
- Replace _id: unknown with Types.ObjectId | string per AGENTS.md
- Narrow listRoles data-layer return type to Pick<IRole, 'name' | 'description'>
to match the actual .select() projection
- Move updateRoleHandler param check inside try/catch for consistency
- Include user IDs in all CRITICAL rollback failure logs for operator recovery
- Clarify deleteRoleByName JSDoc: replace "self-healing" with "idempotent",
document that recovery requires caller retry
- Add tests: system-role guard, promote non-admin to ADMIN,
findUserIdsByRole throw prevents migration
* fix: include _id in listRoles return type to match RoleListItem
Pick<IRole, 'name' | 'description'> omits _id, making it incompatible
with the handler dep's RoleListItem which requires _id.
* fix: case-insensitive system role guard, reject null permissions, check updateUser result
- System role name checks now use case-insensitive comparison via
toUpperCase() — prevents creating 'admin' or 'user' which would
collide with the legacy roles route that uppercases params
- Reject permissions: null in createRole (typeof null === 'object'
was bypassing the validation)
- Check updateUser return in addRoleMember — return 404 if the user
was deleted between the findUser and updateUser calls
* fix: check updateUser return in removeRoleMember for concurrent delete safety
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* feat: add listGroups and deleteGroup methods to userGroup
* feat: add admin groups handler factory and Express routes
* fix: address convention violations in admin groups handlers
* fix: address Copilot review findings in admin groups handlers
- Escape regex in listGroups to prevent injection/ReDoS
- Validate ObjectId format in all handlers accepting id/userId params
- Replace N+1 findUser loop with batched findUsers query
- Remove unused findGroupsByMemberId from dep interface
- Map Mongoose ValidationError to 400 in create/update handlers
- Validate name in updateGroupHandler (reject empty/whitespace)
- Handle null updateGroupById result (race condition)
- Tighten error message matching in add/remove member handlers
* test: add unit tests for admin groups handlers
* fix: address code review findings for admin groups
Atomic delete/update handlers (single DB trip), pass through
idOnTheSource, add removeMemberById for non-ObjectId members,
deduplicate member results, fix error message exposure, add hard
cap/sort to listGroups, replace GroupListFilter with Pick of
GroupFilterOptions, validate memberIds as array, trim name in
update, fix import order, and improve test hygiene with fresh
IDs per test.
* fix: cascade cleanup, pagination, and test coverage for admin groups
Add deleteGrantsForPrincipal to systemGrant data layer and wire cascade
cleanup (Config, AclEntry, SystemGrant) into deleteGroupHandler. Add
limit/offset pagination to getGroupMembers. Guard empty PATCH bodies with
400. Remove dead type guard and unnecessary type cast. Add 11 new tests
covering cascade delete, idempotent member removal, empty update, search
filter, 500 error paths, and pagination.
* fix: harden admin groups with cascade resilience, type safety, and fallback removal
Wrap cascade cleanup in inner try/catch so partial failure logs but still
returns 200 (group is already deleted). Replace Record<string, unknown> on
deleteAclEntries with proper typed filter. Log warning for unmapped user
ObjectIds in createGroup memberIds. Add removeMemberById fallback when
removeUserFromGroup throws User not found for ObjectId-format userId.
Extract VALID_GROUP_SOURCES constant. Add 3 new tests (60 total).
* refactor: add countGroups, pagination, and projection type to data layer
Extract buildGroupQuery helper, add countGroups method, support
limit/offset/skip in listGroups, standardize session handling to
.session(session ?? null), and tighten projection parameter from
Record<string, unknown> to Record<string, 0 | 1>.
* fix: cascade resilience, pagination, validation, and error clarity for admin groups
- Use Promise.allSettled for cascade cleanup so all steps run even if
one fails; log individual rejections
- Echo deleted group id in delete response
- Add countGroups dep and wire limit/offset pagination for listGroups
- Deduplicate memberIds before computing total in getGroupMembers
- Use { memberIds: 1 } projection in getGroupMembers
- Cap memberIds at 500 entries in createGroup
- Reject search queries exceeding 200 characters
- Clarify addGroupMember error for non-ObjectId userId
- Document deleted-user fallback limitation in removeGroupMember
* test: extend handler and DB-layer test coverage for admin groups
Handler tests: projection assertion, dedup total, memberIds cap,
search max length, non-ObjectId memberIds passthrough, cascade partial
failure resilience, dedup scenarios, echo id in delete response.
DB-layer tests: listGroups sort/filter/pagination, countGroups,
deleteGroup, removeMemberById, deleteGrantsForPrincipal.
* fix: cast group principalId to ObjectId for ACL entry cleanup
deleteAclEntries is a thin deleteMany wrapper with no type casting,
but grantPermission stores group principalId as ObjectId. Passing the
raw string from req.params would leave orphaned ACL entries on group
deletion.
* refactor: remove redundant pagination clamping from DB listGroups
Handler already clamps limit/offset at the API boundary. The DB
method is a general-purpose building block and should not re-validate.
* fix: add source and name validation, import order, and test coverage for admin groups
- Validate source against VALID_GROUP_SOURCES in createGroupHandler
- Cap name at 500 characters in both create and update handlers
- Document total as upper bound in getGroupMembers response
- Document ObjectId requirement for deleteAclEntries in cascade
- Fix import ordering in test file (local value after type imports)
- Add tests for updateGroup with description, email, avatar fields
- Add tests for invalid source and name max-length in both handlers
* fix: add field length caps, flatten nested try/catch, and fix logger level in admin groups
Add max-length validation for description, email, avatar, and
idOnTheSource in create/update handlers. Extract removeObjectIdMember
helper to flatten nested try/catch per never-nesting convention. Downgrade
unmapped-memberIds log from error to warn. Fix type import ordering and
add missing await in removeMemberById for consistency.
* feat: add tenant context middleware for ALS-based isolation
Introduces tenantContextMiddleware that propagates req.user.tenantId
into AsyncLocalStorage, activating the Mongoose applyTenantIsolation
plugin for all downstream DB queries within a request.
- Strict mode (TENANT_ISOLATION_STRICT=true) returns 403 if no tenantId
- Non-strict mode passes through for backward compatibility
- No-op for unauthenticated requests
- Includes 6 unit tests covering all paths
* feat: register tenant middleware and wrap startup/auth in runAsSystem()
- Register tenantContextMiddleware in Express app after capability middleware
- Wrap server startup initialization in runAsSystem() for strict mode compat
- Wrap auth strategy getAppConfig() calls in runAsSystem() since they run
before user context is established (LDAP, SAML, OpenID, social login, AuthService)
* feat: thread tenantId through all getAppConfig callers
Pass tenantId from req.user to getAppConfig() across all callers that
have request context, ensuring correct per-tenant cache key resolution.
Also fixes getBaseConfig admin endpoint to scope to requesting admin's
tenant instead of returning the unscoped base config.
Files updated:
- Controllers: UserController, PluginController
- Middleware: checkDomainAllowed, balance
- Routes: config
- Services: loadConfigModels, loadDefaultModels, getEndpointsConfig, MCP
- Audio services: TTSService, STTService, getVoices, getCustomConfigSpeech
- Admin: getBaseConfig endpoint
* feat: add config cache invalidation on admin mutations
- Add clearOverrideCache(tenantId?) to flush per-principal override caches
by enumerating Keyv store keys matching _OVERRIDE_: prefix
- Add invalidateConfigCaches() helper that clears base config, override
caches, tool caches, and endpoint config cache in one call
- Wire invalidation into all 5 admin config mutation handlers
(upsert, patch, delete field, delete overrides, toggle active)
- Add strict mode warning when __default__ tenant fallback is used
- Add 3 new tests for clearOverrideCache (all/scoped/base-preserving)
* chore: update getUserPrincipals comment to reflect ALS-based tenant filtering
The TODO(#12091) about missing tenantId filtering is resolved by the
tenant context middleware + applyTenantIsolation Mongoose plugin.
Group queries are now automatically scoped by tenantId via ALS.
* fix: replace runAsSystem with baseOnly for pre-tenant code paths
App configs are tenant-owned — runAsSystem() would bypass tenant
isolation and return cross-tenant DB overrides. Instead, add
baseOnly option to getAppConfig() that returns YAML-derived config
only, with zero DB queries.
All startup code, auth strategies, and MCP initialization now use
getAppConfig({ baseOnly: true }) to get the YAML config without
touching the Config collection.
* fix: address PR review findings — middleware ordering, types, cache safety
- Chain tenantContextMiddleware inside requireJwtAuth after passport auth
instead of global app.use() where req.user is always undefined (Finding 1)
- Remove global tenantContextMiddleware registration from index.js
- Update BalanceMiddlewareOptions to include tenantId, remove redundant cast (Finding 4)
- Add warning log when clearOverrideCache cannot enumerate keys on Redis (Finding 3)
- Use startsWith instead of includes for cache key filtering (Finding 12)
- Use generator loop instead of Array.from for key enumeration (Finding 3)
- Selective barrel export — exclude _resetTenantMiddlewareStrictCache (Finding 5)
- Move isMainThread check to module level, remove per-request check (Finding 9)
- Move mid-file require to top of app.js (Finding 8)
- Parallelize invalidateConfigCaches with Promise.all (Finding 10)
- Remove clearOverrideCache from public app.js exports (internal only)
- Strengthen getUserPrincipals comment re: ALS dependency (Finding 2)
* fix: restore runAsSystem for startup DB ops, consolidate require, clarify baseOnly
- Restore runAsSystem() around performStartupChecks, updateInterfacePermissions,
initializeMCPs, and initializeOAuthReconnectManager — these make Mongoose
queries that need system context in strict tenant mode (NEW-3)
- Consolidate duplicate require('@librechat/api') in requireJwtAuth.js (NEW-1)
- Document that baseOnly ignores role/userId/tenantId in JSDoc (NEW-2)
* test: add requireJwtAuth tenant chaining + invalidateConfigCaches tests
- requireJwtAuth: 5 tests verifying ALS tenant context is set after
passport auth, isolated between concurrent requests, and not set
when user has no tenantId (Finding 6)
- invalidateConfigCaches: 4 tests verifying all four caches are cleared,
tenantId is threaded through, partial failure is handled gracefully,
and operations run in parallel via Promise.all (Finding 11)
* fix: address Copilot review — passport errors, namespaced cache keys, /base scoping
- Forward passport errors in requireJwtAuth before entering tenant
middleware — prevents silent auth failures from reaching handlers (P1)
- Account for Keyv namespace prefix in clearOverrideCache — stored keys
are namespaced as "APP_CONFIG:_OVERRIDE_:..." not "_OVERRIDE_:...",
so override caches were never actually matched/cleared (P2)
- Remove role from getBaseConfig — /base should return tenant-scoped
base config, not role-merged config that drifts per admin role (P2)
- Return tenantStorage.run() for cleaner async semantics
- Update mock cache in service.spec.ts to simulate Keyv namespacing
* fix: address second review — cache safety, code quality, test reliability
- Decouple cache invalidation from mutation response: fire-and-forget
with logging so DB mutation success is not masked by cache failures
- Extract clearEndpointConfigCache helper from inline IIFE
- Move isMainThread check to lazy once-per-process guard (no import
side effect)
- Memoize process.env read in overrideCacheKey to avoid per-request
env lookups and log flooding in strict mode
- Remove flaky timer-based parallelism assertion, use structural check
- Merge orphaned double JSDoc block on getUserPrincipals
- Fix stale [getAppConfig] log prefix → [ensureBaseConfig]
- Fix import order in tenant.spec.ts (package types before local values)
- Replace "Finding 1" reference with self-contained description
- Use real tenantStorage primitives in requireJwtAuth spec mock
* fix: move JSDoc to correct function after clearEndpointConfigCache extraction
* refactor: remove Redis SCAN from clearOverrideCache, rely on TTL expiry
Redis SCAN causes 60s+ stalls under concurrent load (see #12410).
APP_CONFIG defaults to FORCED_IN_MEMORY_CACHE_NAMESPACES, so the
in-memory store.keys() path handles the standard case. When APP_CONFIG
is Redis-backed, overrides expire naturally via overrideCacheTtl (60s
default) — an acceptable window for admin config mutations.
* fix: remove return from tenantStorage.run to satisfy void middleware signature
* fix: address second review — cache safety, code quality, test reliability
- Switch invalidateConfigCaches from Promise.all to Promise.allSettled
so partial failures are logged individually instead of producing one
undifferentiated error (Finding 3)
- Gate overrideCacheKey strict-mode warning behind a once-per-process
flag to prevent log flooding under load (Finding 4)
- Add test for passport error forwarding in requireJwtAuth — the
if (err) { return next(err) } branch now has coverage (Finding 5)
- Add test for real partial failure in invalidateConfigCaches where
clearAppConfigCache rejects (not just the swallowed endpoint error)
* chore: reorder imports in index.js and app.js for consistency
- Moved logger and runAsSystem imports to maintain a consistent import order across files.
- Improved code readability by ensuring related imports are grouped together.
* fix: add useOptionalMessagesOperations hook for context-safe message operations
Add a variant of useMessagesOperations that returns no-op functions
when MessagesViewProvider is absent instead of throwing, enabling
shared components to render safely outside the chat route.
* fix: use optional message operations in ToolCallInfo and UIResourceCarousel
Switch ToolCallInfo and UIResourceCarousel from useMessagesOperations
to useOptionalMessagesOperations so they no longer crash when rendered
in the /search route, which lacks MessagesViewProvider.
* fix: update test mocks to use useOptionalMessagesOperations
* fix: consolidate noops and narrow useMemo dependency in useOptionalMessagesOperations
- Replace three noop variants (noopAsync, noopReturn, noop) with a single
`const noop = () => undefined` that correctly returns void/undefined
- Destructure individual fields from context before the useMemo so the
dependency array tracks stable operation references, not the full
context object (avoiding unnecessary re-renders on unrelated state changes)
- Add useOptionalMessagesConversation for components that need conversation
data outside MessagesViewProvider
* fix: use optional hooks in MCPUIResource components to prevent search crash
MCPUIResource and MCPUIResourceCarousel render inside Markdown prose and
can appear in the /search route. Switch them from the strict
useMessagesOperations/useMessagesConversation hooks to the optional
variants that return safe defaults when MessagesViewProvider is absent.
* test: update test mocks for optional hook renames
* fix: update ToolCallInfo and UIResourceCarousel test mocks to useOptionalMessagesOperations
* fix: use optional message operations in useConversationUIResources
useConversationUIResources internally called the strict
useMessagesOperations(), which still threw when MCPUIResource rendered
outside MessagesViewProvider. Switch to useOptionalMessagesOperations
so the entire MCPUIResource render chain is safe in the /search route.
* style: fix import order per project conventions
* fix: replace as-unknown-as casts with typed NOOP_OPS stubs
- Define OptionalMessagesOps type and NOOP_OPS constant with properly
typed no-op functions, eliminating all `as unknown as T` casts
- Use conversationId directly from useOptionalMessagesConversation
instead of re-deriving it from conversation object
- Update JSDoc to reflect search route support
* test: add no-provider regression tests for optional message hooks
Verify useOptionalMessagesOperations and useOptionalMessagesConversation
return safe defaults when rendered outside MessagesViewProvider, covering
the core crash path this PR fixes.
* perf: Add local snapshot to aggregate key cache to avoid redundant Redis GETs
getAll() was being called 20+ times per chat request (once per tool,
per server config lookup, per connection check). Each call hit Redis
even though the data doesn't change within a request cycle.
Add an in-memory snapshot with 5s TTL that collapses all reads within
the window into a single Redis GET. Writes (add/update/remove/reset)
invalidate the snapshot immediately so mutations are never stale.
Also removes the debug logger that was producing noisy per-call logs.
* fix: Prevent snapshot mutation and guarantee cleanup on write failure
- Never mutate the snapshot object in-place during writes. Build a new
object (spread) so concurrent readers never observe uncommitted state.
- Move invalidateLocalSnapshot() into withWriteLock's finally block so
cleanup is guaranteed even when successCheck throws on Redis failure.
- After successful writes, populate the snapshot with the committed state
to avoid an unnecessary Redis GET on the next read.
- Use Date.now() after the await in getAll() so the TTL window isn't
shortened by Redis latency.
- Strengthen tests: spy on underlying Keyv cache to verify N getAll()
calls collapse into 1 Redis GET, verify snapshot reference immutability.
* fix: Remove dead populateLocalSnapshot calls from write callbacks
populateLocalSnapshot was called inside withWriteLock callbacks, but
the finally block in withWriteLock always calls invalidateLocalSnapshot
immediately after — undoing the populate on every execution path.
Remove the dead method and its three call sites. The snapshot is
correctly cleared by finally on both success and failure paths. The
next getAll() after a write hits Redis once to fetch the committed
state, which is acceptable since writes only occur during init and
rare manual reinspection.
* fix: Derive local snapshot TTL from MCP_REGISTRY_CACHE_TTL config
Use cacheConfig.MCP_REGISTRY_CACHE_TTL (default 5000ms) instead of a
hardcoded 5s constant. When TTL is 0 (operator explicitly wants no
caching), the snapshot is disabled entirely — every getAll() hits Redis.
* fix: Add TTL expiry test, document 2×TTL staleness, clarify comments
- Add missing test for snapshot TTL expiry path (force-expire via
localSnapshotExpiry mutation, verify Redis is hit again)
- Document 2×TTL max cross-instance staleness in localSnapshot JSDoc
- Document reset() intentionally bypasses withWriteLock
- Add inline comments explaining why early invalidateLocalSnapshot()
in write callbacks is distinct from the finally-block cleanup
- Update cacheConfig.MCP_REGISTRY_CACHE_TTL JSDoc to reflect both
use sites and the staleness implication
- Rename misleading test name for snapshot reference immutability
- Add epoch sentinel comment on localSnapshotExpiry initialization
* fix(api): add buildOAuthToolCallName utility for MCP OAuth flows
Extract a shared utility that builds the synthetic tool-call name
used during MCP OAuth flows (oauth_mcp_{normalizedServerName}).
Uses startsWith on the raw serverName (not the normalized form) to
guard against double-wrapping, so names that merely normalize to
start with oauth_mcp_ (e.g., oauth@mcp@server) are correctly
prefixed while genuinely pre-wrapped names are left as-is.
Add 8 unit tests covering normal names, pre-wrapped names, _mcp_
substrings, special characters, non-ASCII, and empty string inputs.
* fix(backend): use buildOAuthToolCallName in MCP OAuth flows
Replace inline tool-call name construction in both reconnectServer
(MCP.js) and createOAuthEmitter (ToolService.js) with the shared
buildOAuthToolCallName utility. Remove unused normalizeServerName
import from ToolService.js. Fix import ordering in both files.
This ensures the oauth_mcp_ prefix is consistently applied so the
client correctly identifies MCP OAuth flows and binds the CSRF
cookie to the right server.
* fix(client): robust MCP OAuth detection and split handling in ToolCall
- Fix split() destructuring to preserve tail segments for server names
containing _mcp_ (e.g., foo_mcp_bar no longer truncated to foo).
- Add auth URL redirect_uri fallback: when the tool-call name lacks
the _mcp_ delimiter, parse redirect_uri for the MCP callback path.
Set function_name to the extracted server name so progress text
shows the server, not the raw tool-call ID.
- Display server name instead of literal "oauth" as function_name,
gated on auth presence to avoid misidentifying real tools named
"oauth".
- Consolidate three independent new URL(auth) parses into a single
parsedAuthUrl useMemo shared across detection, actionId, and
authDomain hooks.
- Replace any type on ProgressText test mock with structural type.
- Add 8 tests covering delimiter detection, multi-segment names,
function_name display, redirect_uri fallback, normalized _mcp_
server names, and non-MCP action auth exclusion.
* chore: fix import order in utils.test.ts
* fix(client): drop auth gate on OAuth displayName so completed flows show server name
The createOAuthEnd handler re-emits the toolCall delta without auth,
so auth is cleared on the client after OAuth completes. Gating
displayName on `func === 'oauth' && auth` caused completed OAuth
steps to render "Completed oauth" instead of "Completed my-server".
Remove the `&& auth` gate — within the MCP delimiter branch the
func="oauth" check alone is sufficient. Also remove `auth` from the
useMemo dep array since only `parsedAuthUrl` is referenced. Update
the test to assert correct post-completion display.
* ⚡ perf: Use in-memory cache for App MCP configs to avoid Redis SCAN
The 'App' namespace holds static YAML-loaded configs identical on every
instance. Storing them in Redis and retrieving via SCAN + batch-GET
caused 60s+ stalls under concurrent load (#11624). Since these configs
are already loaded into memory at startup, bypass Redis entirely by
always returning ServerConfigsCacheInMemory for the 'App' namespace.
* ♻️ refactor: Extract APP_CACHE_NAMESPACE constant and harden tests
- Extract magic string 'App' to a shared `APP_CACHE_NAMESPACE` constant
used by both ServerConfigsCacheFactory and MCPServersRegistry
- Document that `leaderOnly` is ignored for the App namespace
- Reset `cacheConfig.USE_REDIS` in test `beforeEach` to prevent
ordering-dependent flakiness
- Fix import order in test file (longest to shortest)
* 🐛 fix: Populate App cache on follower instances in cluster mode
In cluster deployments, only the leader runs MCPServersInitializer to
inspect and cache MCP server configs. Followers previously read these
from Redis, but with the App namespace now using in-memory storage,
followers would have an empty cache.
Add populateLocalCache() so follower processes independently initialize
their own in-memory App cache from the same YAML configs after the
leader signals completion. The method is idempotent — if the cache is
already populated (leader case), it's a no-op.
* 🐛 fix: Use static flag for populateLocalCache idempotency
Replace getAllServerConfigs() idempotency check with a static
localCachePopulated flag. The previous check merged App + DB caches,
causing false early returns in deployments with publicly shared DB
configs, and poisoned the TTL read-through cache with stale results.
The static flag is zero-cost (no async/Redis/DB calls), immune to
DB config interference, and is reset alongside hasInitializedThisProcess
in resetProcessFlag() for test teardown.
Also set localCachePopulated=true after leader initialization completes,
so subsequent calls on the leader don't redundantly re-run populateLocalCache.
* 📝 docs: Document process-local reset() semantics for App cache
With the App namespace using in-memory storage, reset() only clears the
calling process's cache. Add JSDoc noting this behavioral change so
callers in cluster deployments know each instance must reset independently.
* ✅ test: Add follower cache population tests for MCPServersInitializer
Cover the populateLocalCache code path:
- Follower populates its own App cache after leader signals completion
- localCachePopulated flag prevents redundant re-initialization
- Fresh follower process independently initializes all servers
* 🧹 style: Fix import order to longest-to-shortest convention
* 🔬 test: Add Redis perf benchmark to isolate getAll() bottleneck
Benchmarks that run against a live Redis instance to measure:
1. SCAN vs batched GET phases independently
2. SCAN cost scaling with total keyspace size (noise keys)
3. Concurrent getAll() at various concurrency levels (1/10/50/100)
4. Alternative: single aggregate key vs SCAN+GET
5. Alternative: raw MGET vs Keyv batch GET (serialization overhead)
Run with: npx jest --config packages/api/jest.config.mjs \
--testPathPatterns="perf_benchmark" --coverage=false
* ⚡ feat: Add aggregate-key Redis cache for MCP App configs
ServerConfigsCacheRedisAggregateKey stores all configs under a single
Redis key, making getAll() a single GET instead of SCAN + N GETs.
This eliminates the O(keyspace_size) SCAN that caused 60s+ stalls in
large deployments while preserving cross-instance visibility — all
instances read/write the same Redis key, so reinspection results
propagate automatically after readThroughCache TTL expiry.
* ♻️ refactor: Use aggregate-key cache for App namespace in factory
Update ServerConfigsCacheFactory to return ServerConfigsCacheRedisAggregateKey
for the App namespace when Redis is enabled, instead of ServerConfigsCacheInMemory.
This preserves cross-instance visibility (reinspection results propagate
through Redis) while eliminating SCAN. Non-App namespaces still use the
standard per-key ServerConfigsCacheRedis.
* 🗑️ revert: Remove populateLocalCache — no longer needed with aggregate key
With App configs stored under a single Redis key (aggregate approach),
followers read from Redis like before. The populateLocalCache mechanism
and its localCachePopulated flag are no longer necessary.
Also reverts the process-local reset() JSDoc since reset() is now
cluster-wide again via Redis.
* 🐛 fix: Add write mutex to aggregate cache and exclude perf benchmark from CI
- Add promise-based write lock to ServerConfigsCacheRedisAggregateKey to
prevent concurrent read-modify-write races during parallel initialization
(Promise.allSettled runs multiple addServer calls concurrently, causing
last-write-wins data loss on the aggregate key)
- Rename perf benchmark to cache_integration pattern so CI skips it
(requires live Redis)
* 🔧 fix: Rename perf benchmark to *.manual.spec.ts to exclude from all CI
The cache_integration pattern is picked up by test:cache-integration:mcp
in CI. Rename to *.manual.spec.ts which isn't matched by any CI runner.
* ✅ test: Add cache integration tests for ServerConfigsCacheRedisAggregateKey
Tests against a live Redis instance covering:
- CRUD operations (add, get, update, remove)
- getAll with empty/populated cache
- Duplicate add rejection, missing update/remove errors
- Concurrent write safety (20 parallel adds without data loss)
- Concurrent read safety (50 parallel getAll calls)
- Reset clears all configs
* 🔧 fix: Rename perf benchmark to *.manual.spec.ts to exclude from all CI
The perf benchmark file was renamed to *.manual.spec.ts but no
testPathIgnorePatterns existed for that convention. Add .*manual\.spec\.
to both test and test:ci scripts, plus jest.config.mjs, so manual-only
tests never run in CI unit test jobs.
* fix: Address review findings for aggregate key cache
- Add successCheck() to all write paths (add/update/remove) so Redis
SET failures throw instead of being silently swallowed
- Override reset() to use targeted cache.delete(AGGREGATE_KEY) instead
of inherited SCAN-based cache.clear() — consistent with eliminating
SCAN operations
- Document cross-instance write race invariant in class JSDoc: the
promise-based writeLock is process-local only; callers must enforce
single-writer semantics externally (leader-only init)
- Use definite-assignment assertion (let resolve!:) instead of non-null
assertion at call site
- Fix import type convention in integration test
- Verify Promise.allSettled rejections explicitly in concurrent write test
- Fix broken run command in benchmark file header
* style: Fix import ordering per AGENTS.md convention
Local/project imports sorted longest to shortest.
* chore: Update import ordering and clean up unused imports in MCPServersRegistry.ts
* chore: import order
* chore: import order
* fix: Invalidate message cache on STREAM_EXPIRED instead of showing error
When a 404 (stream expired) is received during SSE resume, the generation
has already completed and messages are persisted in the database. Instead
of injecting an error message into the cache, invalidate the messages query
so react-query refetches from the DB. Also clear stale stream status cache
and step maps to prevent retries and memory leaks.
* fix: Mark conversation as processed when no active job found
Prevents useResumeOnLoad from repeatedly re-checking the same
conversation when the stream status returns inactive. The ref
still resets on conversation change, so navigating away and back
will correctly re-check.
Also wait for background refetches to settle (isFetching) before
acting on inactive status, preventing stale cached active:false
from suppressing a valid resume.
* test: Update useResumableSSE spec for cache invalidation on 404
Verify message cache invalidation, stream status removal,
clearStepMaps, and setIsSubmitting(false) on the 404 path.
* fix: Resolve lint warnings from CI
Remove unused ErrorTypes import in test, add queryClient to
useCallback dependency array in useResumableSSE.
* Reorder import statements in useResumableSSE.ts
MCP SDK >= 1.23.0 requires zod ^3.25 which conflicts with the
workspace-wide zod ^3.22.4. Pin to 1.22.0 (last zod 3.22-compatible
version) in api/package.json, packages/api peerDeps, and pnpm overrides.
* ✨ feat: Add Config schema, model, and methods for role-based DB config overrides
Add the database foundation for principal-based configuration overrides
(user, group, role) in data-schemas. Includes schema with tenantId and
tenant isolation, CRUD methods, and barrel exports.
* 🔧 fix: Add shebang and enforce LF line endings for git hooks
The pre-commit hook was missing #!/bin/sh, and core.autocrlf=true was
converting it to CRLF, both causing "Exec format error" on Windows.
Add .gitattributes to force LF for .husky/* and *.sh files.
* ✨ feat: Add admin config API routes with section-level capability checks
Add /api/admin/config endpoints for managing per-principal config
overrides (user, group, role). Handlers in @librechat/api use DI pattern
with section-level hasConfigCapability checks for granular access control.
Supports full overrides replacement, per-field PATCH via dot-paths, field
deletion, toggle active, and listing.
* 🐛 fix: Move deleteConfigField fieldPath from URL param to request body
The path-to-regexp wildcard syntax (:fieldPath(*)) is not supported by
the version used in Express. Send fieldPath in the DELETE request body
instead, which also avoids URL-encoding issues with dotted paths.
* ✨ feat: Wire config resolution into getAppConfig with override caching
Add mergeConfigOverrides utility in data-schemas for deep-merging DB
config overrides into base AppConfig by priority order.
Update getAppConfig to query DB for applicable configs when role/userId
is provided, with short-TTL caching and a hasAnyConfigs feature flag
for zero-cost when no DB configs exist.
Also: add unique compound index on Config schema, pass userId from
config middleware, and signal config changes from admin API handlers.
* 🔄 refactor: Extract getAppConfig logic into packages/api as TS service
Move override resolution, caching strategy, and signalConfigChange from
api/server/services/Config/app.js into packages/api/src/app/appConfigService.ts
using the DI factory pattern (createAppConfigService). The JS file becomes
a thin wiring layer injecting loadBaseConfig, cache, and DB dependencies.
* 🧹 chore: Rename configResolution.ts to resolution.ts
* ✨ feat: Move admin types & capabilities to librechat-data-provider
Move SystemCapabilities, CapabilityImplications, and utility functions
(hasImpliedCapability, expandImplications) from data-schemas to
data-provider so they are available to external consumers like the
admin panel without a data-schemas dependency.
Add API-friendly admin types: TAdminConfig, TAdminSystemGrant,
TAdminAuditLogEntry, TAdminGroup, TAdminMember, TAdminUserSearchResult,
TCapabilityCategory, and CAPABILITY_CATEGORIES.
data-schemas re-exports these from data-provider and extends with
config-schema-derived types (ConfigSection, SystemCapability union).
Bump version to 0.8.500.
* feat: Add JSON-serializable admin config API response types to data-schemas
Add AdminConfig, AdminConfigListResponse, AdminConfigResponse, and
AdminConfigDeleteResponse types so both LibreChat API handlers and the
admin panel can share the same response contract. Bump version to 0.0.41.
* refactor: Move admin capabilities & types from data-provider to data-schemas
SystemCapabilities, CapabilityImplications, utility functions,
CAPABILITY_CATEGORIES, and admin API response types should not be in
data-provider as it gets compiled into the frontend bundle, exposing
the capability surface. Moved everything to data-schemas (server-only).
All consumers already import from @librechat/data-schemas, so no
import changes needed elsewhere. Consolidated duplicate AdminConfig
type (was in both config.ts and admin.ts).
* chore: Bump @librechat/data-schemas to 0.0.42
* refactor: Reorganize admin capabilities into admin/ and types/admin.ts
Split systemCapabilities.ts following data-schemas conventions:
- Types (BaseSystemCapability, SystemCapability, AdminConfig, etc.)
→ src/types/admin.ts
- Runtime code (SystemCapabilities, CapabilityImplications, utilities)
→ src/admin/capabilities.ts
Revert data-provider version to 0.8.401 (no longer modified).
* chore: Fix import ordering, rename appConfigService to service
- Rename app/appConfigService.ts → app/service.ts (directory provides context)
- Fix import order in admin/config.ts, types/admin.ts, types/config.ts
- Add naming convention to AGENTS.md
* feat: Add DB base config support (role/__base__)
- Add BASE_CONFIG_PRINCIPAL_ID constant for reserved base config doc
- getApplicableConfigs always includes __base__ in queries
- getAppConfig queries DB even without role/userId when DB configs exist
- Bump @librechat/data-schemas to 0.0.43
* fix: Address PR review issues for admin config
- Add listAllConfigs method; listConfigs endpoint returns all active
configs instead of only __base__
- Normalize principalId to string in all config methods to prevent
ObjectId vs string mismatch on user/group lookups
- Block __proto__ and all dunder-prefixed segments in field path
validation to prevent prototype pollution
- Fix configVersion off-by-one: default to 0, guard pre('save') with
!isNew, use $inc on findOneAndUpdate
- Remove unused getApplicableConfigs from admin handler deps
* fix: Enable tree-shaking for data-schemas, bump packages
- Switch data-schemas Rollup output to preserveModules so each source
file becomes its own chunk; consumers (admin panel) can now import
just the modules they need without pulling in winston/mongoose/etc.
- Add sideEffects: false to data-schemas package.json
- Bump data-schemas to 0.0.44, data-provider to 0.8.402
* feat: add capabilities subpath export to data-schemas
Adds `@librechat/data-schemas/capabilities` subpath export so browser
consumers can import BASE_CONFIG_PRINCIPAL_ID and capability constants
without pulling in Node.js-only modules (winston, async_hooks, etc.).
Bump version to 0.0.45.
* fix: include dist/ in data-provider npm package
Add explicit files field so npm includes dist/types/ in the published
package. Without this, the root .gitignore exclusion of dist/ causes
npm to omit type declarations, breaking TypeScript consumers.
* chore: bump librechat-data-provider to 0.8.403
* feat: add GET /api/admin/config/base for raw AppConfig
Returns the full AppConfig (YAML + DB base merged) so the admin panel
can display actual config field values and structure. The startup config
endpoint (/api/config) returns TStartupConfig which is a different shape
meant for the frontend app.
* chore: imports order
* fix: address code review findings for admin config
Critical:
- Fix clearAppConfigCache: was deleting from wrong cache store (CONFIG_STORE
instead of APP_CONFIG), now clears BASE and HAS_DB_CONFIGS keys
- Eliminate race condition: patchConfigField and deleteConfigField now use
atomic MongoDB $set/$unset with dot-path notation instead of
read-modify-write cycles, removing the lost-update bug entirely
- Add patchConfigFields and unsetConfigField atomic DB methods
Major:
- Reorder cache check before principal resolution in getAppConfig so
getUserPrincipals DB query only fires on cache miss
- Replace '' as ConfigSection with typed BROAD_CONFIG_ACCESS constant
- Parallelize capability checks with Promise.all instead of sequential
awaits in for loops
- Use loose equality (== null) for cache miss check to handle both null
and undefined returns from cache implementations
- Set HAS_DB_CONFIGS_KEY to true on successful config fetch
Minor:
- Remove dead pre('save') hook from config schema (all writes use
findOneAndUpdate which bypasses document hooks)
- Consolidate duplicate type imports in resolution.ts
- Remove dead deepGet/deepSet/deepUnset functions (replaced by atomic ops)
- Add .sort({ priority: 1 }) to getApplicableConfigs query
- Rename _impliedBy to impliedByMap
* fix: self-referencing BROAD_CONFIG_ACCESS constant
* fix: replace type-cast sentinel with proper null parameter
Update hasConfigCapability to accept ConfigSection | null where null
means broad access check (MANAGE_CONFIGS or READ_CONFIGS only).
Removes the '' as ConfigSection type lie from admin config handlers.
* fix: remaining review findings + add tests
- listAllConfigs accepts optional { isActive } filter so admin listing
can show inactive configs (#9)
- Standardize session application to .session(session ?? null) across
all config DB methods (#15)
- Export isValidFieldPath and getTopLevelSection for testability
- Add 38 tests across 3 spec files:
- config.spec.ts (api): path validation, prototype pollution rejection
- resolution.spec.ts: deep merge, priority ordering, array replacement
- config.spec.ts (data-schemas): full CRUD, ObjectId normalization,
atomic $set/$unset, configVersion increment, toggle, __base__ query
* fix: address second code review findings
- Fix cross-user cache contamination: overrideCacheKey now handles
userId-without-role case with its own cache key (#1)
- Add broad capability check before DB lookup in getConfig to prevent
config existence enumeration (#2/#3)
- Move deleteConfigField fieldPath from request body to query parameter
for proxy/load balancer compatibility (#5)
- Derive BaseSystemCapability from SystemCapabilities const instead of
manual string union (#6)
- Return 201 on upsert creation, 200 on update (#11)
- Remove inline narration comments per AGENTS.md (#12)
- Type overrides as Partial<TCustomConfig> in DB methods and handler
deps (#13)
- Replace double as-unknown-as casts in resolution.ts with generic
deepMerge<T> (#14)
- Make override cache TTL injectable via AppConfigServiceDeps (#16)
- Add exhaustive never check in principalModel switch (#17)
* fix: remaining review findings — tests, rename, semantics
- Rename signalConfigChange → markConfigsDirty with JSDoc documenting
the stale-window tradeoff and overrideCacheTtl knob
- Fix DEFAULT_OVERRIDE_CACHE_TTL naming convention
- Add createAppConfigService tests (14 cases): cache behavior, feature
flag, cross-user key isolation, fallback on error, markConfigsDirty
- Add admin handler integration tests (13 cases): auth ordering,
201/200 on create/update, fieldPath from query param, markConfigsDirty
calls, capability checks
* fix: global flag corruption + empty overrides auth bypass
- Remove HAS_DB_CONFIGS_KEY=false optimization: a scoped query returning
no configs does not mean no configs exist globally. Setting the flag
false from a per-principal query short-circuited all subsequent users.
- Add broad manage capability check before section checks in
upsertConfigOverrides: empty overrides {} no longer bypasses auth.
* test: add regression and invariant tests for config system
Regression tests:
- Bug 1: User A's empty result does not short-circuit User B's overrides
- Bug 2: Empty overrides {} returns 403 without MANAGE_CONFIGS
Invariant tests (applied across ALL handlers):
- All 5 mutation handlers call markConfigsDirty on success
- All 5 mutation handlers return 401 without auth
- All 5 mutation handlers return 403 without capability
- All 3 read handlers return 403 without capability
* fix: third review pass — all findings addressed
Service (service.ts):
- Restore HAS_DB_CONFIGS=false for base-only queries (no role/userId)
so deployments with zero DB configs skip DB queries (#1)
- Resolve cache once at factory init instead of per-invocation (#8)
- Use BASE_CONFIG_PRINCIPAL_ID constant in overrideCacheKey (#10)
- Add JSDoc to clearAppConfigCache documenting stale-window (#4)
- Fix log message to not say "from YAML" (#14)
Admin handlers (config.ts):
- Use configVersion===1 for 201 vs 200, eliminating TOCTOU race (#2)
- Add Array.isArray guard on overrides body (#5)
- Import CapabilityUser from capabilities.ts, remove duplicate (#6)
- Replace as-unknown-as cast with targeted type assertion (#7)
- Add MAX_PATCH_ENTRIES=100 cap on entries array (#15)
- Reorder deleteConfigField to validate principalType first (#12)
- Export CapabilityUser from middleware/capabilities.ts
DB methods (config.ts):
- Remove isActive:true from patchConfigFields to prevent silent
reactivation of disabled configs (#3)
Schema (config.ts):
- Change principalId from Schema.Types.Mixed to String (#11)
Tests:
- Add patchConfigField unsafe fieldPath rejection test (#9)
- Add base-only HAS_DB_CONFIGS=false test (#1)
- Update 201/200 tests to use configVersion instead of findConfig (#2)
* fix: add read handler 401 invariant tests + document flag behavior
- Add invariant: all 3 read handlers return 401 without auth
- Document on markConfigsDirty that HAS_DB_CONFIGS stays true after
all configs are deleted until clearAppConfigCache or restart
* fix: remove HAS_DB_CONFIGS false optimization entirely
getApplicableConfigs([]) only queries for __base__, not all configs.
A deployment with role/group configs but no __base__ doc gets the
flag poisoned to false by a base-only query, silently ignoring all
scoped overrides. The optimization is not safe without a comprehensive
Config.exists() check, which adds its own DB cost. Removed entirely.
The flag is now write-once-true (set when configs are found or by
markConfigsDirty) and only cleared by clearAppConfigCache/restart.
* chore: reorder import statements in app.js for clarity
* refactor: remove HAS_DB_CONFIGS_KEY machinery entirely
The three-state flag (false/null/true) was the source of multiple bugs
across review rounds. Every attempt to safely set it to false was
defeated by getApplicableConfigs querying only a subset of principals.
Removed: HAS_DB_CONFIGS_KEY constant, all reads/writes of the flag,
markConfigsDirty (now a no-op concept), notifyChange wrapper, and all
tests that seeded false manually.
The per-user/role TTL cache (overrideCacheTtl, default 60s) is the
sole caching mechanism. On cache miss, getApplicableConfigs queries
the DB. This is one indexed query per user per TTL window — acceptable
for the config override use case.
* docs: rewrite admin panel remaining work with current state
* perf: cache empty override results to avoid repeated DB queries
When getApplicableConfigs returns no configs for a principal, cache
baseConfig under their override key with TTL. Without this, every
user with no per-principal overrides hits MongoDB on every request
after the 60s cache window expires.
* fix: add tenantId to cache keys + reject PUBLIC principal type
- Include tenantId in override cache keys to prevent cross-tenant
config contamination. Single-tenant deployments (tenantId undefined)
use '_' as placeholder — no behavior change for them.
- Reject PrincipalType.PUBLIC in admin config validation — PUBLIC has
no PrincipalModel and is never resolved by getApplicableConfigs,
so config docs for it would be dead data.
- Config middleware passes req.user.tenantId to getAppConfig.
* fix: fourth review pass findings
DB methods (config.ts):
- findConfigByPrincipal accepts { includeInactive } option so admin
GET can retrieve inactive configs (#5)
- upsertConfig catches E11000 duplicate key on concurrent upserts and
retries without upsert flag (#2)
- unsetConfigField no longer filters isActive:true, consistent with
patchConfigFields (#11)
- Typed filter objects replace Record<string, unknown> (#12)
Admin handlers (config.ts):
- patchConfigField: serial broad capability check before Promise.all
to pre-warm ALS principal cache, preventing N parallel DB calls (#3)
- isValidFieldPath rejects leading/trailing dots and consecutive
dots (#7)
- Duplicate fieldPaths in patch entries return 400 (#8)
- DEFAULT_PRIORITY named constant replaces hardcoded 10 (#14)
- Admin getConfig and patchConfigField pass includeInactive to
findConfigByPrincipal (#5)
- Route import uses barrel instead of direct file path (#13)
Resolution (resolution.ts):
- deepMerge has MAX_MERGE_DEPTH=10 guard to prevent stack overflow
from crafted deeply nested configs (#4)
* fix: final review cleanup
- Remove ADMIN_PANEL_REMAINING.md (local dev notes with Windows paths)
- Add empty-result caching regression test
- Add tenantId to AdminConfigDeps.getAppConfig type
- Restore exhaustive never check in principalModel switch
- Standardize toggleConfigActive session handling to options pattern
* fix: validate priority in patchConfigField handler
Add the same non-negative number validation for priority that
upsertConfigOverrides already has. Without this, invalid priority
values could be stored via PATCH and corrupt merge ordering.
* chore: remove planning doc from PR
* fix: correct stale cache key strings in service tests
* fix: clean up service tests and harden tenant sentinel
- Remove no-op cache delete lines from regression tests
- Change no-tenant sentinel from '_' to '__default__' to avoid
collision with a real tenant ID when multi-tenancy is enabled
- Remove unused CONFIG_STORE from AppConfigServiceDeps
* chore: bump @librechat/data-schemas to 0.0.46
* fix: block prototype-poisoning keys in deepMerge
Skip __proto__, constructor, and prototype keys during config merge
to prevent prototype pollution via PUT /api/admin/config overrides.
Docker builds use npm (not pnpm). The zod-to-json-schema override
was duplicated in both pnpm.overrides and npm overrides. Keep it
only in npm overrides where the Docker build actually reads it.
Casdoor generates OIDC discovery documents using the incoming request's
Host header. When customFetch rewrites requests from hanzo.id to the
internal iam.hanzo.svc, the response issuer becomes "https://iam.hanzo.svc"
instead of "https://hanzo.id", which openid-client v6 rejects as an
issuer mismatch (strict RFC 8414 validation).
Fix: inject the original public hostname as the Host header when
rewriting to the internal service URL.
* 🐛 fix: Prevent crash when token balance exhaustion races with client disposal
Add null guard in saveMessageToDatabase to handle the case where
disposeClient nullifies this.options while a userMessagePromise is
still pending from a prior async save operation.
* 🐛 fix: Snapshot this.options to prevent mid-await disposal crash
The original guard at function entry was insufficient — this.options is
always valid at entry. The crash occurs after the first await
(db.saveMessage) when disposeClient nullifies client.options while the
promise is suspended.
Fix: capture this.options into a local const before any await. The local
reference is immune to client.options = null set by disposeClient.
Also add .catch on userMessagePromise in sendMessage to prevent
unhandled rejections when checkBalance throws before the promise is
awaited, and add two regression tests.
* fix: resolve user-provided API key in Agents API flow
When the Agents API calls initializeCustom, req.body follows the
OpenAI-compatible format (model, messages, stream) and does not
include the `key` field that the regular UI chat flow sends.
Previously, getUserKeyValues was only called when expiresAt
(from req.body.key) was truthy, causing the Agents API to always
fail with NO_USER_KEY for custom endpoints using
apiKey: "user_provided".
This fix decouples the key fetch from the expiry check:
- If expiresAt is present (UI flow): checks expiry AND fetches key
- If expiresAt is absent (Agents API): skips expiry check, still
fetches key
Fixes#12389
* address review feedback from @danny-avila
- Flatten nested if into two sibling statements (never-nesting style)
- Add inline comment explaining why expiresAt may be absent
- Add negative assertion: checkUserKeyExpiry NOT called in Agents API flow
- Add regression test: expired key still throws EXPIRED_USER_KEY
- Add test for userProvidesURL=true variant in Agents API flow
- Remove unnecessary undefined cast in test params
* fix: CI failure + address remaining review items
- Fix mock leak: use mockImplementationOnce instead of mockImplementation
to prevent checkUserKeyExpiry throwing impl from leaking into SSRF tests
(clearAllMocks does not reset implementations)
- Use ErrorTypes.EXPIRED_USER_KEY constant instead of raw string
- Add test: system-defined key/URL should NOT call getUserKeyValues
* fix: resolve MeiliSearch startup sync failure from model loading order
`indexSync.js` captures `mongoose.models.Message` and
`mongoose.models.Conversation` at module load time (lines 9-10).
Since PR #11830 moved model registration into `createModels()`,
these references are always `undefined` because `require('./indexSync')`
ran before `createModels(mongoose)` in `api/db/index.js`.
Move the `require('./indexSync')` below `createModels(mongoose)` so
Mongoose models are registered before `indexSync.js` captures them.
* fix: defer model lookups in indexSync, add load-order regression test
- Move mongoose.models.Message and mongoose.models.Conversation lookups
from module-level into performSync() and the indexSync() catch block,
eliminating the fragile load-order dependency that caused the original
regression
- Add guard assertion that throws a clear error if models are missing
- Add comment in api/db/index.js documenting the createModels ordering
constraint
- Add api/db/index.spec.js regression test to prevent future re-introduction
of the load-order bug
* fix: strengthen regression test to verify call order, add guard in setTimeout path
- Rewrite index.spec.js to track call sequence via callOrder array,
ensuring createModels executes before indexSync module loads
(verified: test fails if order is reversed)
- Add missing model guard assertion in the setTimeout fallback path
in indexSync.js for consistency with performSync
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
Addresses vulnerabilities disclosed in CERTFR-2026-AVI-0310:
- Improper object lifecycle management of MD5 hash state in core
cryptographic operations (Blocker/P1)
- Use-after-free in ExpressionContext during pipeline cloning with
nested $unionWith stages (Major/P3)
* fix: fast-fail MCP discovery for non-OAuth servers on auth errors
Always attach oauthHandler in discoverToolsInternal regardless of
useOAuth flag. Previously, non-OAuth servers hitting 401 would hang
for 30s because connectClient's oauthHandledPromise had no listener
to emit oauthFailed, waiting until withTimeout killed it.
* chore: import order
* feat: redesign tool call UI with type-specific icons, smart grouping, and rich output rendering
Replace the generic spinner/checkmark tool call UI with a modern, Cursor-inspired design:
- Add per-tool-type icons (Plug for MCP, Terminal for code, Globe for web search, etc.)
- Group 2+ consecutive tool calls into collapsible "Used N tools" sections
- Stack unique tool icons in grouped headers with overlapping circle design
- Replace raw JSON output with intelligent renderers (table, error, text)
- Restructure ToolCallInfo: output first, parameters collapsible at bottom
- Add shared useExpandCollapse hook for consistent animations
- Add CodeWindowHeader for ExecuteCode windowed view
- Remove FinishedIcon (purple checkmark) entirely
* feat: display custom MCP server icons in tool calls
Add useMCPIconMap hook to resolve MCP server names to their configured
icon paths. ToolIcon and StackedToolIcons now accept custom icon URLs,
showing actual server logos (e.g., Home Assistant, GitHub) instead of
the generic Plug icon for MCP tool calls.
* refactor: unify container styling across code blocks, mermaid, and tool output
Replace hardcoded gray colors with theme tokens throughout:
- CodeBlock: bg-gray-900/700 -> bg-surface-secondary/tertiary + border-border-light
- Mermaid dialog: bg-gray-700 -> bg-surface-secondary, text-gray-200 -> text-text-secondary
- Mermaid containers: rounded-xl -> rounded-lg, remove shadow-md for consistency
- ResultSwitcher: bg-gray-700 -> bg-surface-secondary with border separator
- RunCode: hover:bg-gray-700 -> hover:bg-surface-hover
- ErrorOutput: add border for visual consistency
- MermaidHeader/CodeWindowHeader: consistent focus outlines using border-heavy
* refactor: simplify tool output to plain text, remove custom renderers
Remove over-engineered tool output system (TableOutput, ErrorOutput,
detectOutputType) in favor of simple text extraction. Tool output now
extracts the text content from MCP content blocks and displays it as
clean readable text — no tables, no error styling, no JSON formatting.
Parameters only show key-value badges for simple objects; complex JSON
is hidden instead of dumped raw. Matches Cursor-style simplicity.
* fix: handle error messages and format JSON arrays in tool output
- Strip verbose MCP error prefixes (Error: [MCP][server][tool] tool call
failed: Error POSTing...) and show just the meaningful error message
- Display errors in red text
- Format uniform JSON arrays as readable lists (name — path) instead
of raw JSON dumps
- Format plain JSON objects as key: value lines
* feat: improve JSON display in tool call output and parameters
- Replace flat formatObject with recursive formatValue for proper
indented display of nested JSON structures
- Add ComplexInput component for tool parameters with nested objects,
arrays, or long strings (previously hidden)
- Broaden hasParams check to show parameters for all object types
- Add font-mono to output renderer for better alignment
* feat: add localization keys for tool errors, web search, and code UI
* refactor: move Mermaid components into dedicated directory module
* refactor: extract CodeBar, FloatingCodeBar, and copy utilities from CodeBlock
* feat: replace manual SVG icons with @icons-pack/react-simple-icons
Supports 50+ programming languages with tree-shaken brand icons
instead of hand-crafted SVGs for 19 languages.
* refactor: simplify code execution UI with persistent code toggle
* refactor: use useExpandCollapse hook in Thinking and Reasoning
* feat: improve tool call error states, subtitles, and group summaries
* feat: redesign web search with inline source display
* feat: improve agent handoff with keyboard accessibility
* feat: reorganize exports order in hooks and utils
* refactor: unify CopyCodeButton with animated icon transitions and iconOnly support
* feat: add run code state machine with animated success/error feedback
* refactor: improve ResultSwitcher with lucide icons and accessibility
* refactor: update CopyButton component
* refactor: replace CopyCodeButton with CopyButton component across multiple files
* test: add ImageGen test stubs
* test: add RetrievalCall test stubs
* feat: merge ImageGen with ToolIcon and localized progress text
* feat: modernize RetrievalCall with ToolIcon and collapsible output
* test: add getToolIconType action delimiter tests
* test: add ImageGen collapsible output tests
* feat: add action ToolIcon type with Zap icon
* fix: replace AgentHandoff div with semantic button
* feat: add aria-live regions to tool components
* feat: redesign execute_code tool UI with syntax highlighting and language icons
- Remove filename labels (script.py, main.rs) and line counter from CodeWindowHeader
- Replace generic FileCode icon with language-specific LangIcon
- Add syntax highlighting via highlight.js to code blocks
- Add SquareTerminal icon to ExecuteCode progress text
- Use shared CopyButton component in CodeWindowHeader
- Remove active:scale-95 press animation from CopyButton and RunCode
* feat: dynamic tool status text sizing based on markdown font-size variable
- Add tool-status-text CSS class using calc(0.9 * --markdown-font-size)
- Update progress-text-wrapper to use dynamic sizing instead of base size
- Apply tool-status-text to WebSearch, ToolCallGroup, AgentHandoff, ImageGen
- Replace hardcoded text-sm/text-xs with dynamic class across all tools
- Animate chevron rotation in ProgressText and ToolCallGroup
- Update subtitle text color from tertiary to secondary
* fix: consistent spacing and text styles across all tool components
- Standardize tool status row spacing to my-1/my-1.5 across all components
- Update ToolCallInfo text from tertiary to secondary, add vertical padding
- Animate ToolCallInfo parameters chevron rotation
- Update OutputRenderer link colors from tertiary to secondary
* feat: unify tool call grouping for all tool types
All consecutive tool calls (MCP, execute_code, web_search, image_gen,
file_search, code_interpreter) are now grouped under a single
collapsible "Used N tools" header instead of only grouping generic
tool calls.
- Remove SPECIAL_TOOL_NAMES blacklist from groupToolCalls
- Replace getToolCallData with getToolMeta to handle all tool types
- Use renderPart callback in ToolCallGroup for proper component routing
- Add file_search and code_interpreter mappings to getToolIconType
* feat: friendly tool group labels, more icons, and output copy button
- Show friendly names in group summary (Code, Web Search, Image
Generation) instead of raw tool names
- Display MCP server names instead of individual function names
- Deduplicate labels and show up to 3 with +N overflow
- Increase stacked icons from 3 to 4
- Add icon-only copy button to tool output (OutputRenderer)
* fix: execute_code spacing and syntax-highlighted code visibility
Match ToolCall spacing by using my-1.5 on status line and moving my-2
inside overflow-hidden. Replace broken hljs.highlight() with lowlight
(same engine used by rehype-highlight for markdown code blocks) to
render syntax-highlighted code as React elements. Handle object args
in useParseArgs to support both string and Record arg formats.
* feat: replace showCode with auto-expand tools setting
Replace the execute_code-only "Always show code when using code
interpreter" global toggle with a new "Auto-expand tool details"
setting that controls all tool types. Each tool instance now uses
independent local state initialized from the setting, so expanding
one tool no longer affects others. Applies to ToolCall, ExecuteCode,
ToolCallGroup, and CodeAnalyze components.
* fix: apply auto-expand tools setting to WebSearch and RetrievalCall
* fix: only auto-expand tools when content is available
Defer auto-expansion until tool output or content arrives, preventing
empty bordered containers from showing while tools are still running.
Uses useEffect to expand when output becomes available during streaming.
* feat: redesign file_search tool output, citations, and file preview
- Redesign RetrievalCall with per-file cards using OutputRenderer
(truncated content with show more/less, copy button) matching MCP
tool pattern
- Route file_search tool calls from Agents API to RetrievalCall
instead of generic ToolCall
- Add FilePreviewDialog for viewing files (PDF iframe, text content)
with download option, opened from clickable filenames
- Redesign file citations: FileText icon in badge, relevance and
page numbers in hovercard, click opens file preview instead of
downloading
- Add file preview to message file attachments (Files.tsx)
- Fix hovercard animation to slide top-to-bottom and dismiss
instantly on file click to prevent glitching over dialog
- Add localization keys for relevance, extracted content, preview
- Add top margin to ToolCallGroup
* chore: remove leftover .planning files
* fix: polish FilePreviewDialog, CodeBlock, LangIcon, and Sources
* fix: prevent keyboard focus on collapsed tool content
Add inert attribute to all expand/collapse wrapper divs so
collapsed content is removed from tab order and hidden from
assistive technology. Skip disabled ProgressText buttons from
tab order with tabIndex={-1}.
* feat: integrate file metadata into file_search UI
Pass fileType (MIME) and fileBytes from backend file records through
to the frontend. Add file-type-specific icons, file size display,
pages sorted by relevance, multi-snippet content per file, smart
preview detection by MIME type, and copy button in file preview dialog.
* fix: review fixes — inverted type check, wrong dimension, missing import, fail-open perms, timer leaks, dead code cleanup
* fix: update CodeBlock styling for improved visual consistency
* fix(chat): open composite file citations in preview
* fix(chat): restore file previews for parsed search results
* chore(git): ignore bg-shell artifacts
* fix(chat): restore readable code content in light theme
* style(chat): align code and output surfaces by theme
* chore(i18n): remove 6 unused translation keys
* fix(deps): replace private registry URL with public npm registry in lockfile
* fix: CI lint, build, and test failures
- Add missing scaleImage utility (fixes Vite build error)
- Export scaleImage from utils/index.ts
- Remove unused imports from Part.tsx (FunctionToolCall, CodeToolCall, Agents)
- Fix prettier formatting in Part.tsx (multi-line → single-line imports, conditions)
- Remove excess blank lines in Part.tsx
- Remove unused CodeEditorRef import from Artifacts.tsx
- Add useProgress mock to OpenAIImageGen.test.tsx
- Add scaleImage mock to OpenAIImageGen.test.tsx
- Update OpenAIImageGen tests to match redesigned component structure
- Remove dead collapsible output panel tests from ImageGen.test.tsx
- Add @icons-pack/react-simple-icons to Jest transformIgnorePatterns (ESM fix)
* refactor: reorganize imports order across multiple components for consistency
* fix: add scaleImage tests, delete dead ImageGen wrapper, wire up onUIAction in ToolCallInfo
- Add 7 unit tests for scaleImage utility covering null ref, scaling,
no-upscale, height clamping, landscape, and panoramic images
- Delete unused Content/ImageGen.tsx re-export wrapper (ImageGen is
imported from Parts/OpenAIImageGen via the Parts barrel)
- Wire up onUIAction in ToolCallInfo to use handleUIAction + ask from
useMessagesOperations, matching UIResourceCarousel's behavior
(was previously a silent no-op)
* refactor: optimize imports and enhance lazy loading for language icons
* fix: address review findings for tool call UI redesign
- Fix unstable array-index keys in ToolCallGroup (streaming state corruption)
- Add plain-text fallback in InputRenderer for non-JSON tool args
- Localize FRIENDLY_NAMES via translation keys instead of hardcoded English
- Guard autoCollapse against user-initiated manual expansion
- Fix CODE_INTERPRETER hasOutput to check actual outputs instead of hardcoding true
- Add logger.warn for Citations fail-closed behavior on permission errors
- Add Terminal icon to CodeAnalyze ProgressText for visual consistency
- Fix getMCPServerName to use indexOf instead of fragile split
- Use useLayoutEffect for inert attribute in useExpandCollapse (a11y)
- Memoize style object in useExpandCollapse to avoid defeating React.memo
- Memoize groupSequentialToolCalls in ContentParts to avoid recomputation
- Use source.link as stable key instead of array index in WebSearch
- Hoist rehypePlugins outside CodeMarkdown to prevent per-render recreation
* fix: revert useMemo after conditional returns in ContentParts
The useMemo placed after early returns violated React Rules of Hooks —
hook call count would change when transitioning between edit/view mode.
Reverted to the original plain forEach which is correct and equally
performant since content changes on every streaming token anyway.
* chore: remove unused com_ui_variables_info translation key
* fix: update tests and jest config for ESM compatibility after rebase
- Add ESM-only packages to transformIgnorePatterns (@dicebear, unified
ecosystem, react-dnd, lowlight, etc.) to fix Jest parse failures
introduced by dev rebase
- Update ToolCall.test.tsx to match new component API (CSS
expand/collapse instead of conditional rendering, simplified props)
- Update ToolCallInfo.test.tsx to mock OutputRenderer (avoids ESM
chain), align with current component interface (input/output/attachments)
* refactor: replace @icons-pack/react-simple-icons with inline SVGs
Inline the 51 Simple Icons SVG paths used by LangIcon directly into
langIconPaths.ts, eliminating the runtime dependency on
@icons-pack/react-simple-icons (which requires Node >= 24).
- LangIcon now renders a plain <svg> with the path data instead of
lazy-loading React components from the package
- Removes Suspense/React.lazy overhead for code block language icons
- SVG paths sourced from Simple Icons (CC0 1.0 license)
- Package kept in package.json for now (will be removed separately)
* fix: replace Plug icon with Wrench for MCP tools, remove unused i18n keys
- MCP tools without a custom iconPath now show Wrench instead of Plug,
matching the generic tool fallback and avoiding the "plugin" metaphor
- Remove unused translation keys: com_assistants_action_attempt,
com_assistants_attempt_info, com_assistants_domain_info,
com_ui_ui_resources
* fix: address second review findings
- Combine 3x getToolMeta loop into single toolMetadata pass (ToolCallGroup)
- Extract sortPagesByRelevance to shared util (was duplicated in
FilePreviewDialog and RetrievalCall)
- Deduplicate AGENT_STYLE_TOOLS Set (export from OpenAIImageGen/index.ts)
- Localize "source/sources" in WebSearch aria-label
- Add autoExpand useEffect to CodeAnalyze for live setting changes
- Log download errors in FilePreviewDialog instead of silently swallowing
- Replace @ts-ignore with @ts-expect-error + explanation in Code.tsx
- Remove dead currentContent alias in CodeMarkdown
* chore: remove @icons-pack/react-simple-icons dependency from package.json and package-lock.json
- Deleted the @icons-pack/react-simple-icons entry from both package.json and package-lock.json, following the previous refactor to use inline SVGs for icons.
* fix: address triage audit findings
- Remove unused gIdx variable (ESLint error)
- Fix singular/plural in web search sources aria-label
- Separate inline type import in ToolCallGroup per AGENTS.md
* fix: remove invalid placeholderDimensions prop from Image component
* chore: import order
* chore: import order
* fix: resolve TypeScript errors in PR-touched files
- Remove non-existent placeholderDimensions prop from Image in Files.tsx
- Fix localize count param type (number, not string) in WebSearch.tsx
- Pass full resource object instead of partial in UIResourceCarousel.tsx
- Add 'as const' to toggleSwitchConfigs localizationKey in General.tsx
- Fix SearchResultData type in Citation.test.tsx
- Fix TAttachment and UIResource test fixture types across test files
* docs: document formatBytes difference in FilePreviewDialog
The local formatBytes returns a human-readable string with units
("1.5 MB"), while ~/utils/formatBytes returns a raw number. They
serve different purposes, so the local copy is retained with a
JSDoc comment explaining the distinction.
* fix: address remaining review items
- Replace cancelled IIFE with documented ternary in OpenAIImageGen,
explaining the agent vs legacy path distinction
- Add .catch() fallback to loadLowlight() in useLazyHighlight — falls
back to plain text if the chunk fails to load
- Fix import ordering in ToolCallGroup.tsx (type imports grouped before
local value imports per AGENTS.md)
* fix: blob URL leak and useGetFiles over-fetch
- FilePreviewDialog: add cancelledRef guard to loadPreview so blob URLs
are never created after the dialog closes (prevents orphaned object
URLs on unmount during async PDF fetch)
- RetrievalCall: filter useGetFiles by fileIds from fileSources instead
of fetching the entire user file corpus for display-only name matching
* chore: fix com_nav_auto_expand_tools alphabetical order in translation.json
* fix: render non-object JSON params instead of returning null in InputRenderer
* refactor: render JSON tool output as syntax-highlighted code block
Replace the custom YAML-ish formatValue/formatObjectArray rendering
with JSON.stringify + hljs language-json styling. Structured API
responses (like GitHub search results) now display as proper
syntax-highlighted JSON with indentation instead of a flat key-value
text dump.
- Remove formatValue, formatObjectArray, isUniformObjectArray helpers
- Add isJson flag to extractText return type
- JSON output rendered in <code class="hljs language-json"> block
- Text content blocks (type: "text") still extracted and rendered
as plain text
- Error output unchanged
* fix: extract cancelled IIFE to named function in OpenAIImageGen
Replace nested ternary with a named computeCancelled() function that
documents the agent vs legacy path branching. Resolves eslint
no-nested-ternary warning.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
zod-to-json-schema 3.25.x imports 'zod/v3' which only exists in zod 4.
The codebase uses zod 3.24.4 which does not export that subpath, causing
ERR_PACKAGE_PATH_NOT_EXPORTED on startup. Pin to 3.24.6 (last version
compatible with zod 3.x) via pnpm.overrides.
* 🐛 fix: Sidebar favorites row height stuck at stale measurement
Use a content-aware cache key for the favorites row in the virtualized
sidebar list. The CellMeasurerCache keyMapper now encodes
favorites.length, showAgentMarketplace, and isFavoritesLoading into the
key so the cache naturally invalidates when the content shape changes,
forcing CellMeasurer to re-measure from scratch instead of returning a
stale height from a transient render state.
Remove the onHeightChange callback and its useEffect from FavoritesList
since the content-aware key handles all height-affecting state
transitions.
* test: Add unit tests for Conversations component favorites height caching
Introduced a new test suite for the Conversations component to validate the behavior of the favorites CellMeasurerCache. The tests ensure that the cache correctly invalidates when the favorites count changes, loading state transitions occur, and marketplace visibility toggles. This enhances the reliability of the component's rendering logic and ensures proper height management in the virtualized list.
* fix: Validate active sidebar panel against available links
The `side:active-panel` localStorage key was shared with the old
right-side panel. On first load of the unified sidebar, a stale value
(e.g. 'hide-panel', or a conditional panel not currently available)
would match no link, leaving the expanded panel empty.
Add `resolveActivePanel` as derived state in both Nav and ExpandedPanel
so content and icon highlight always fall back to the first link when
the stored value doesn't match any available link.
* refactor: Remove redundant new-chat button from Agent Marketplace header
The sidebar icon strip already provides a new chat button, making the
sticky header in the marketplace page unnecessary.
* chore: import order and linting
* fix: Update dependencies in Conversations component to include marketplace visibility
Modified the useEffect dependency array in the Conversations component to include `showAgentMarketplace`, ensuring proper reactivity to changes in marketplace visibility. Additionally, updated the test suite to mock favorites state for accurate height caching validation when marketplace visibility changes.
The hanzo-build-linux-arm64 Graviton runner has been offline for 10 days,
causing every Docker Release run to hang until cancelled. Production K8s
clusters are amd64-only so arm64 images are not needed. Drop arm64 from
the platform list to unblock the build pipeline.
* style: enhance prompts UI with new components and improved structure; add CreatePromptButton and AutoSendPrompt; refactor GroupSidePanel and PromptsAccordion
* refactor(Prompts): move button components to buttons/ subdirectory
* refactor(Prompts): move dialog components to dialogs/ subdirectory
* refactor(Prompts): move display components to display/ subdirectory
* refactor(Prompts): move editor components to editor/ subdirectory
* refactor(Prompts): move field components to fields/ subdirectory
* refactor(Prompts): move form components to forms/ subdirectory
* refactor(Prompts): move layout components to layouts/ subdirectory
* refactor(Prompts): move list components to lists/ subdirectory
* refactor(Prompts): move sidebar components to sidebar/ subdirectory
* refactor(Prompts): move utility components to utils/ subdirectory
* refactor(Prompts): update main exports and external imports
* refactor(Prompts): fix class name typo in AutoSendPrompt
* refactor(Prompts): reorganize exports and imports order across components
* refactor(Prompts): reorder exports for better organization and clarity
* refactor(Buttons): enhance prompts accessibility with aria-labels and update translations
* refactor(AdminSettings): reorganize imports and improve form structure for clarity
* refactor(Dialogs): reorganize imports for consistency and clarity across DeleteVersion, SharePrompt, and VariableDialog components
* refactor(Dialogs): enhance prompts accessibility with aria-labels
* refactor(Display): enhance prompt components and accessibility features
* refactor(.gitignore): add Playwright MCP directory
* refactor(Preview): enhance prompt components, improve layout, and add accessibility features
* refactor(Prompts): enhance variable handling, improve accessibility, and update UI components
* refactor(Prompts): enhance loading state handling and improve accessibility in PromptName component
* refactor(Prompts): streamline special variable handling, improve icon management, and enhance UI components
* refactor(Prompts): update AdvancedSwitch component to use Radio for mode selection, enhance PromptName with tooltips, and improve layout in PromptForm
* refactor(Prompts): enhance VersionCard and VersionBadge components for improved UI and accessibility, update loading state handling in VersionsPanel
* refactor(Prompts): improve layout and styling of VersionCard component for better visual alignment and clarity
* refactor(DeleteVersion): update text color for confirmation prompt in DeleteConfirmDialog
* refactor(Prompts): add configurations for always make production and auto-send prompts, update localization strings for clarity
* refactor(Prompts): enhance layout and styling in CategorySelector, CreatePromptForm, and List components for improved responsiveness and clarity
* refactor(Prompts): enhance PromptDetailHeader and ChatGroupItem components, add shared prompt indication, and remove unused PromptMetadata component
* refactor(Prompts): implement prompt group usage tracking, update sorting logic, and enhance related components
* fix(Prompts): security, performance, and pagination fixes
- Fix cursor pagination skipping/duplicating items by including
numberOfGenerations in cursor condition to match sort order
- Close NoSQL injection vector via otherFilters rest spread in
GET /all, GET /groups, and buildPromptGroupFilter
- Validate groupId as ObjectId before passing to query (GET /)
- Add prompt body validation in addPromptToGroup (type + text)
- Return 404 instead of 500 for missing group in POST /use
- Combine data + count into single $facet aggregation
- Add compound index {numberOfGenerations, updatedAt, _id}
- Add index on prompt.author for deleteUserPrompts
- Update useRecordPromptUsage to refresh client caches
- Replace console.error with logger.error
* refactor(PromptForm): remove console warning for unselected prompt in VersionsPanel
* refactor(Prompts): improve error handling for groupId and streamline usage tracking
* refactor(.gitignore): add CLAUDE.md to ignore list
* refactor(Prompts): streamline prompt components by removing unused variables and enhancing props structure
* refactor(Prompts): fix sort stability, keyboard handling, and remove dead code
Add _id tiebreaker to prompt group sort pipelines for deterministic
pagination ordering. Prevent default browser scroll on Space key in
PromptEditor preview mode. Remove unused blurTimeoutRef and its
onMutate callback from DashGroupItem.
* refactor(Prompts): enhance groupId validation and improve prompt group aggregation handling
* fix: aria-hidden, API fixes, accessibility improvements
* fix: ACL author filter, mobile guard, semantic HTML, and add useFocusTrap hook
- Remove author filter from patchPromptGroup so ACL-granted editors
can update prompt groups (aligns with deletePromptGroupController)
- Add missing group guard to mobile HeaderActions in PromptForm
- Replace div with article in DashGroupItem, remove redundant
stopPropagation and onClick on outer container
- Add useFocusTrap hook for keyboard focus management
- Add numberOfGenerations to default projection
- Deduplicate ObjectId validation, remove console.warn,
fix aria-labelledby, localize search announcements
* refactor(Prompts): adjust UI and improve a11y
* refactor(Prompts): reorder imports for consistency and clarity
* refactor(Prompts): implement updateFieldsInPlace for efficient data updates and add related tests
* refactor(Prompts): reorder imports to include updateFieldsInPlace for better organization
* refactor(Prompts): enhance DashGroupItem with toast notifications for prompt updates and add click-to-edit functionality in PromptEditor
* style: use self-closing TooltipAnchor in CreatePromptButton
Replace ></TooltipAnchor> with /> for consistency with the rest of the Prompts directory.
* fix(i18n): replace placeholder text for com_ui_global_group translation key
The value was left as 'something needs to go here. was empty' which
would be visible to users as an aria-label in DashGroupItem.
* fix(DashGroupItem): sync rename input with group.name on external changes
nameInputValue was initialized via useState(group.name) but never
synced when group.name changed from a background refetch. Added
useEffect that updates the input when the dialog is closed.
* perf(useFocusTrap): store onEscape in ref to avoid listener churn
onEscape was in the useEffect dependency array, causing the keydown
listener to be torn down and re-attached on every render when callers
passed an inline function. Now stored in a ref so the effect only
re-runs when active or containerRef changes.
* fix(a11y): replace role=button div with layered button overlay in ListCard
The card used role='button' on a div that contained nested Button
elements — an invalid ARIA pattern. Replaced with a hidden button
at z-0 for the card action while child interactive elements sit
at z-10, eliminating nested interactive element violations.
* fix(PromptForm): reset selectionIndex on route change, guard auto-save, and fix a11y
- Reset selectionIndex to 0 and isEditing to false when promptId
changes, preventing out-of-bounds index when navigating between
groups with different version counts.
- Track selectedPrompt in a ref so the auto-save effect doesn't
fire against a stale prompt when the selection changed mid-edit.
- Stabilize useFocusTrap onEscape via useCallback to avoid
unnecessary listener re-attachment.
- Conditionally render mobile overlay instead of always-present
button with aria-hidden/pointer-events toggling.
* refactor: extract isValidObjectIdString to shared utility in data-schemas
The same regex helper was duplicated in api/server/routes/prompts.js
and packages/data-schemas/src/methods/prompt.ts. Moved to
packages/data-schemas/src/utils/objectId.ts and imported from both
consumers. Also removed a duplicate router.use block introduced
during the extraction.
* perf(updateFieldsInPlace): replace JSON deep clone with targeted spread
Instead of JSON.parse(JSON.stringify(data)) which serializes the
entire paginated data structure, use targeted immutable spreads
that only copy the affected page and collection array. Returns the
original data reference unchanged when the item is not found.
* perf(VariablesDropdown): memoize items array and stabilize handleAddVariable
The items array containing JSX elements was rebuilt on every render.
Wrapped in useMemo keyed on usedVariables and localize. Also wrapped
handleAddVariable in useCallback and memoized usedCount to avoid
redundant array filtering.
* perf(DashGroupItem): stabilize mutation callbacks via refs
handleSaveRename and handleDelete had updateGroup/deleteGroup mutation
objects in their useCallback dependency arrays. Since mutation objects
are new references each render, the callbacks were recreated every
render, defeating memoization. Now store mutation objects in refs and
call via ref.current in the callbacks.
* fix(security): validate groupId in incrementPromptGroupUsage
The data-schema method passed the groupId string directly to
findByIdAndUpdate without validation. If called from a different
entrypoint without the route-level check, Mongoose would throw a
CastError. Now validates with isValidObjectIdString before the
DB call and throws a clean 'Invalid groupId' error.
* fix(security): add rate limiter to prompt usage tracking endpoint
POST /groups/:groupId/use had no rate limiting — a user could spam
it to inflate numberOfGenerations, which controls sort order for all
users. Added promptUsageLimiter (30 req/user/min) following the same
pattern as toolCallLimiter. Also handle 'Invalid groupId' error from
the data layer in the route error handler.
* fix(updateFieldsInPlace): guard against undefined identifier value
If updatedItem[identifierField] is null/undefined, findIndex could
match unintended items where that field is also undefined. Added
early return when the identifier value is nullish.
* fix(a11y): use React useId for stable unique IDs in ListCard
aria-describedby/id values were derived from prompt name which can
contain spaces and special characters, producing invalid HTML IDs
and potential collisions. Now uses React.useId() for guaranteed
unique, valid IDs per component instance.
* fix: Align prompts panel styling with other sidebar panels and fix test
- Match FilterPrompts first row to Memory/Bookmark pattern (items-center gap-2)
- Remove items-stretch override from PromptsAccordion
- Add missing promptUsageLimiter mock to prompts route test
* fix: Address code review findings for prompts refactor PR
- Fix#5: Gate DeletePrompt in HeaderActions behind canDelete permission
- Fix#8: BackToChat navigates to last conversation instead of /c/new
- Fix#7: Restore useLiveAnnouncer for screen reader feedback on delete/rename
- Fix#1: Use isPublic (set by API) instead of deprecated projectIds for globe icon
- Fix#4: Optimistic cache update in useRecordPromptUsage instead of full invalidation
- Fix#6: Add migration to drop superseded { createdAt, updatedAt } compound index
- Fix#9: Single-pass reduce in PromptVariables instead of triple filter
- Fix#10: Rename PromptLabelsForm internal component to avoid collision with PromptForm
- Fix#14: Remove redundant aria-label from aria-hidden Checkbox in AutoSendPrompt
* fix: Align prompts panel filter row element sizes with other panels
- Override Dropdown trigger to size-9 (36px) to match FilterInput height
- Set CreatePromptButton to size-9 shrink-0 bg-transparent matching
Memory/Bookmark panel button pattern
* fix(prompts): Shared Prompts filter ignores direct shares, only returns PUBLIC
Folds fix from PR #11882 into the refactored codebase.
Bug A: filterAccessibleIdsBySharedLogic now accepts ownedPromptGroupIds:
- MY_PROMPTS: accessible intersect owned
- SHARED_PROMPTS: (accessible union public) minus owned
- ALL: accessible union public (deduplicated)
Legacy fallback preserved when ownedPromptGroupIds is omitted.
Bug B: getPromptGroup uses $lookup aggregation to populate productionPrompt,
fixing empty text on direct URL navigation to shared prompts.
Also adds getOwnedPromptGroupIds to data-schemas methods and passes it
from both /all and /groups route handlers.
* fix: Add missing canDelete to mobile HeaderActions, remove dead instanceProjectId prop
- Pass canDelete to mobile HeaderActions row (was only on desktop)
- Remove instanceProjectId prop from ChatGroupItem and DashGroupItem
since global check now uses group.isPublic
- Remove useGetStartupConfig from List.tsx (no longer needed)
* fix: Use runtime ObjectId instead of type-only Types.ObjectId, fix i18next interpolation
- getPromptGroup and getOwnedPromptGroupIds were using Types.ObjectId
(imported as type-only), which is erased at compile time. Use the
runtime ObjectId from mongoose.Types (already destructured at line 20).
This fixes the 404s in PATCH /groups/:groupId tests.
- Fix com_ui_prompt_deleted_group translation to use {{0}} (i18next
double-brace syntax) instead of {0}.
* chore: Fix translation key ordering, add sideEffects: false to data-provider
- Reorder new translation keys to maintain alphabetical order:
com_ui_click_to_edit, com_ui_labels, com_ui_live, com_ui_prompt_delete_confirm,
com_ui_prompt_deleted_group, com_ui_prompt_details, com_ui_prompt_renamed,
com_ui_prompt_update_error, com_ui_prompt_variables_list
- Add "sideEffects": false to librechat-data-provider package.json to
enable tree-shaking of unused exports (types, constants, pure functions)
* fix: Reduce prompts panel spacing, align memory toggle with checkbox pattern
- Remove unnecessary wrapper div around AutoSendPrompt in PromptsAccordion,
reducing vertical space between the toggle and the first prompt item
- Replace Memory panel's Switch toggle with Checkbox+Button pattern
matching the prompts panel's AutoSendPrompt for visual consistency
* fix: Reduce gap between AutoSendPrompt and first prompt item
Change ChatGroupItem margin from my-2 to mb-2 to eliminate the
doubled spacing (gap-2 from parent + top margin from first item).
Restore wrapper div around AutoSendPrompt for right-alignment.
* fix: Restore prompt name on empty save, remove dead bodyProps from checkGlobalPromptShare
- PromptName: reset newName to name when save is cancelled due to empty
or unchanged input, preventing blank title in read mode
- checkGlobalPromptShare: remove dead bodyProps config — Permissions.SHARE
was not in the permissions array so the bodyProps rule was never evaluated.
Per-resource share checks are handled by canAccessPromptGroupResource.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* chore: Update react-resizable-panels dependency to version 4.7.4
- Upgraded the "react-resizable-panels" package in package-lock.json, package.json, and client package.json files to ensure compatibility with the latest features and improvements.
- Adjusted peer dependencies for React and ReactDOM to align with the new version requirements.
* refactor: Update Share and SidePanel components to `react-resizable-panels` v4
- Refactored the ShareArtifactsContainer to utilize a new layout change handler, enhancing artifact panel resizing functionality.
- Updated ArtifactsPanel to use the new `usePanelRef` hook, improving panel reference management.
- Simplified SidePanelGroup by removing unnecessary layout normalization and integrating default layout handling with localStorage.
- Removed the deprecated `normalizeLayout` utility function to streamline the codebase.
- Adjusted Resizable components to ensure consistent sizing and layout behavior across panels.
* style: Enhance scrollbar appearance across application
- Added custom scrollbar styles to both artifacts and markdown files, improving aesthetics and user experience.
- Implemented dark mode adjustments for scrollbar visibility, ensuring consistency across different color schemes.
* style: Standardize button sizes and layout in Artifacts components
- Updated button dimensions to a consistent height of 9 units across various components including Artifacts, Code, and DownloadArtifact.
- Adjusted padding and layout properties in the Artifacts header for improved visual consistency.
- Enhanced the Radio component to accept a new `buttonClassName` prop for better customization of button styles.
* chore: import order
* fix: Graceful SidePanelContext handling when ChatContext unavailable
The UnifiedSidebar component is rendered at the Root level before ChatContext
is provided (which happens only in ChatRoute). This caused an error when
useSidePanelContext tried to call useChatContext before it was available.
Changes:
- Made SidePanelProvider gracefully handle missing ChatContext with try/catch
- Changed useSidePanelContext to return a safe default instead of throwing
- Prevents render error on application load and improves robustness
* fix: Provide default context value for ChatContext to prevent setFilesLoading errors
The ChatContext was initialized with an empty object as default, causing 'setFilesLoading is not a function' errors when components tried to call functions from the context. This fix provides a proper default context with no-op functions for all expected properties.
Fixes FileRow component errors that occurred when navigating to sections with file upload functionality (Agent Builder, Attach Files, etc.).
* fix: Move ChatFormProvider to Root to fix Prompts sidebar rendering
The ChatFormProvider was only wrapping ChatView, but the sidebar (including Prompts) renders separately and needs access to the ChatFormContext. ChatGroupItem uses useSubmitMessage which calls useChatFormContext, causing a React error when Prompts were accessed.
This fix moves the ChatFormProvider to the Root component to wrap both the sidebar and the main chat view, ensuring the form context is available throughout the entire application.
* fix: Active section switching and dead code cleanup
Sync ActivePanelProvider state when defaultActive prop changes so
clicking a collapsed-bar icon actually switches the expanded section.
Remove the now-unused hideSidePanel atom and its Settings toggle.
* style: Redesign sidebar layout with optimized spacing and positioning
- Remove duplicate new chat button from sidebar, keep it in main header
- Reposition account settings to bottom of expanded sidebar
- Simplify collapsed bar padding and alignment
- Clean up unused framer-motion imports from Header
- Optimize vertical space usage in expanded panel
- Align search bar icon color with sidebar theme
* fix: Chat history not showing in sidebar
Add h-full to ConversationsSection outer div so it fills the Nav
content panel, giving react-virtualized's AutoSizer a measurable
height. Change Nav content panel from overflow-y-auto to
overflow-hidden since the virtualized list handles its own scrolling.
* refactor: Move nav icons to fixed icon strip alongside sidebar toggle
Extract section icons from the Nav content panel into the
ExpandedPanel icon strip, matching the CollapsedBar layout. Both
states now share identical button styling and 50px width, eliminating
layout shift on toggle. Nav.tsx simplified to content-only rendering.
Set text-text-primary on Nav content for consistent child text color.
* refactor: sidebar components and remove unused NewChat component
* refactor: streamline sidebar components and introduce NewChat button
* refactor: enhance sidebar functionality with expanded state management and improved layout
* fix: re-implement sidebar resizing functionality with mouse events
* feat: enhance sidebar layout responsiveness on mobile
* refactor: remove unused components and streamline sidebar functionality
* feat: enhance sidebar behavior with responsive transformations for small screens
* feat: add new chat button for small screens with message cache clearing
* feat: improve state management in sidebar and marketplace components
* feat: enhance scrolling behavior in AgentPanel and Nav components
* fix: normalize sidebar panel font sizes and default panel selection
Set text-sm as base font size on the shared Nav container so all
panels render consistently. Guard against empty localStorage value
when restoring the active sidebar panel.
* fix: adjust avatar size and class for collapsed state in AccountSettings component
* style: adjust padding and class names in Nav, Parameters, and ConversationsSection components
* fix: close mobile sidebar on pinned favorite selection
* refactor: remove unused key in translation file
* fix: Address review findings for unified sidebar
- Restore ChatFormProvider per-ChatView to fix multi-conversation input isolation;
add separate ChatFormProvider in UnifiedSidebar for Prompts panel access
- Add inert attribute on mobile sidebar (when collapsed) and main content
(when sidebar overlay is open) to prevent keyboard focus leaking
- Replace unsafe `as unknown as TChatContext` cast with null-based context
that throws descriptively when used outside a provider
- Throttle mousemove resize handler with requestAnimationFrame to prevent
React state updates at 120Hz during sidebar drag
- Unify active panel state: remove split between activeSection in
UnifiedSidebar and internal state in ActivePanelContext; single source
of truth with localStorage sync on every write
- Delete orphaned SidePanelProvider/useSidePanelContext (no consumers
after SidePanel.tsx removal)
- Add data-testid="new-chat-button" to NewChat component
- Add includeHidePanel option to useSideNavLinks; remove no-op hidePanel
callback and post-hoc filter in useUnifiedSidebarLinks
- Close sidebar on first mobile visit when localStorage has no prior state
- Remove unnecessary min-width/max-width CSS transitions (only width needed)
- Remove dead SideNav re-export from SidePanel/index.ts
- Remove duplicate aria-label from Sidebar nav element
- Fix trailing blank line in mobile.css
* style: fix prettier formatting in Root.tsx
* fix: Address remaining review findings and re-render isolation
- Extract useChatHelpers(0) into SidebarChatProvider child component so
Recoil atom subscriptions (streaming tokens, latestMessage, etc.) only
re-render the active panel — not the sidebar shell, resize logic, or
icon strip (Finding 4)
- Fix prompt pre-fill when sidebar form context differs from chat form:
useSubmitMessage now reads the actual textarea DOM value via
mainTextareaId as fallback for the currentText newline check (Finding 1)
- Add id="close-sidebar-button" and data-testid to ExpandedPanel toggle
so OpenSidebar focus management works after expand (Finding 10/N3)
- Replace Dispatch<SetStateAction<number>> prop with
onResizeKeyboard(direction) callback on Sidebar (Finding 13)
- Fix first-mobile-visit sidebar flash: atom default now checks
window.matchMedia at init time instead of defaulting to true then
correcting in a useEffect; removes eslint-disable suppression (N1/N2)
- Add tests for ActivePanelContext and ChatContext (Finding 8)
* refactor: remove no-op memo from SidebarChatProvider
memo(SidebarChatProvider) provided no memoization because its only prop
(children) is inline JSX — a new reference on every parent render.
The streaming isolation works through Recoil subscription scoping, not
memo. Clarified in the JSDoc comment.
* fix: add shebang to pre-commit hook for Windows compatibility
Git on Windows cannot spawn hook scripts without a shebang line,
causing 'cannot spawn .husky/pre-commit: No such file or directory'.
* style: fix sidebar panel styling inconsistencies
- Remove inner overflow-y-auto from AgentPanel form to eliminate double
scrollbars when nested inside Nav.tsx's scroll container
- Add consistent padding (px-3 py-2) to Nav.tsx panel container
- Remove hardcoded 150px cell widths from Files PanelTable; widen date
column from 25% to 35% so dates are no longer cut off
- Compact pagination row with flex-wrap and smaller text
- Add px-1 padding to Parameters panel for consistency
- Change overflow-x-visible to overflow-x-hidden on Files and Bookmarks
panels to prevent horizontal overflow
* fix: Restore panel styling regressions in unified sidebar
- Nav.tsx wrapper: remove extra px-3 padding, add hide-scrollbar class,
restore py-1 to match old ResizablePanel wrapper
- AgentPanel: restore scrollbar-gutter-stable and mx-1 margin (was px-1)
- Parameters/Panel: restore p-3 padding (was px-1 pt-1)
- Files/Panel: restore overflow-x-visible (was hidden, clipping content)
- Files/PanelTable: restore 75/25 column widths, restore 150px cell
width constraints, restore pagination text-sm and gap-2
- Bookmarks/Panel: restore overflow-x-visible
* style: initial improvements post-sidenav change
* style: update text size in DynamicTextarea for improved readability
* style: update FilterPrompts alignment in PromptsAccordion for better layout consistency
* style: adjust component heights and padding for consistency across SidePanel elements
- Updated height from 40px to 36px in AgentSelect for uniformity
- Changed button size class from "bg-transparent" to "size-9" in BookmarkTable, MCPBuilderPanel, and MemoryPanel
- Added padding class "py-1.5" in DynamicDropdown for improved spacing
- Reduced height from 10 to 9 in DynamicInput and DynamicTags for a cohesive look
- Adjusted padding in Parameters/Panel for better layout
* style: standardize button sizes and icon dimensions across chat components
- Updated button size class from 'size-10' to 'size-9' in multiple components for consistency.
- Adjusted icon sizes from 'icon-lg' to 'icon-md' in various locations to maintain uniformity.
- Modified header height for better alignment with design specifications.
* style: enhance layout consistency and component structure across various panels
- Updated ActivePanelContext to utilize useCallback and useMemo for improved performance.
- Adjusted padding and layout in multiple SidePanel components for better visual alignment.
- Standardized icon sizes and button dimensions in AddMultiConvo and other components.
- Improved overall spacing and structure in PromptsAccordion, MemoryPanel, and FilesPanel for a cohesive design.
* style: standardize component heights and text sizes across various panels
- Adjusted button heights from 10px to 9px in multiple components for consistency.
- Updated text sizes from 'text-sm' to 'text-xs' in DynamicCheckbox, DynamicCombobox, DynamicDropdown, DynamicInput, DynamicSlider, DynamicSwitch, DynamicTags, and DynamicTextarea for improved readability.
- Added portal prop to account settings menu for better rendering behavior.
* refactor: optimize Tooltip component structure for performance
- Introduced a new memoized TooltipPopup component to prevent unnecessary re-renders of the TooltipAnchor when the tooltip mounts/unmounts.
- Updated TooltipAnchor to utilize the new TooltipPopup, improving separation of concerns and enhancing performance.
- Maintained existing functionality while improving code clarity and maintainability.
* refactor: improve sidebar transition handling for better responsiveness
- Enhanced the transition properties in the UnifiedSidebar component to include min-width and max-width adjustments, ensuring smoother resizing behavior.
- Cleaned up import statements by removing unnecessary lines for better code clarity.
* fix: prevent text selection during sidebar resizing
- Added functionality to disable text selection on the body while the sidebar is being resized, enhancing user experience during interactions.
- Restored text selection capability once resizing is complete, ensuring normal behavior resumes.
* fix: ensure Header component is always rendered in ChatView
- Removed conditional rendering of the Header component to ensure it is always displayed, improving the consistency of the chat interface.
* refactor: add NewChatButton to ExpandedPanel for improved user interaction
- Introduced a NewChatButton component in the ExpandedPanel, allowing users to initiate new conversations easily.
- Implemented functionality to clear message cache and invalidate queries upon button click, enhancing performance and user experience.
- Restored import statements for OpenSidebar and PresetsMenu in Header component for better organization.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
- Refactored the `deleteTokens` method to delete tokens based on all provided fields using AND conditions instead of OR, enhancing precision in token management.
- Updated related tests to reflect the new logic, ensuring that only tokens matching all specified criteria are deleted.
- Added new test cases for deleting tokens by type and userId, and for preventing cross-user token deletions, improving overall test coverage and robustness.
- Introduced a new `type` field in the `TokenQuery` interface to support the updated deletion functionality.
Type AclEntry model as Model<IAclEntry> instead of Model<unknown> in
deleteUserAgents and deleteUserPrompts, wire getSoleOwnedResourceIds
into agent.spec.ts via createAclEntryMethods, replace permissionService
calls with direct AclEntry.create, and add missing principalModel field.
Move getSoleOwnedResourceIds from PermissionService to data-schemas aclEntry methods,
update PermissionService to use the db object pattern instead of destructured imports
from ~/models, and update UserController + tests accordingly.
* refactor: Simplify content formatting in MCP service and parser
- Consolidated content handling in `formatToolContent` to return a plain-text string instead of an array for all providers, enhancing clarity and consistency.
- Removed unnecessary checks for content array providers, streamlining the logic for handling text and image artifacts.
- Updated related tests to reflect changes in expected output format, ensuring comprehensive coverage for the new implementation.
* fix: Return empty string for image-only tool responses instead of '(No response)'
When artifacts exist (images/UI resources) but no text content is present,
return an empty string rather than the misleading '(No response)' fallback.
Adds missing test assertions for image-only content and standardizes
length checks to explicit `> 0` comparisons.
* chore: imports/types
Add summarization config and package-level summarize handler contracts
Register summarize handlers across server controller paths
Port cursor dual-read/dual-write summary support and UI status handling
Selectively merge cursor branch files for BaseClient summary content
block detection (last-summary-wins), dual-write persistence, summary
block unit tests, and on_summarize_status SSE event handling with
started/completed/failed branches.
Co-authored-by: Cursor <cursoragent@cursor.com>
refactor: type safety
feat: add localization for summarization status messages
refactor: optimize summary block detection in BaseClient
Updated the logic for identifying existing summary content blocks to use a reverse loop for improved efficiency. Added a new test case to ensure the last summary content block is updated correctly when multiple summary blocks exist.
chore: add runName to chainOptions in AgentClient
refactor: streamline summarization configuration and handler integration
Removed the deprecated summarizeNotConfigured function and replaced it with a more flexible createSummarizeFn. Updated the summarization handler setup across various controllers to utilize the new function, enhancing error handling and configuration resolution. Improved overall code clarity and maintainability by consolidating summarization logic.
feat(summarization): add staged chunk-and-merge fallback
feat(usage): track summarization usage separately from messages
feat(summarization): resolve prompt from config in runtime
fix(endpoints): use @librechat/api provider config loader
refactor(agents): import getProviderConfig from @librechat/api
chore: code order
feat(app-config): auto-enable summarization when configured
feat: summarization config
refactor(summarization): streamline persist summary handling and enhance configuration validation
Removed the deprecated createDeferredPersistSummary function and integrated a new createPersistSummary function for MongoDB persistence. Updated summarization handlers across various controllers to utilize the new persistence method. Enhanced validation for summarization configuration to ensure provider, model, and prompt are properly set, improving error handling and overall robustness.
refactor(summarization): update event handling and remove legacy summarize handlers
Replaced the deprecated summarization handlers with new event-driven handlers for summarization start and completion across multiple controllers. This change enhances the clarity of the summarization process and improves the integration of summarization events in the application. Additionally, removed unused summarization functions and streamlined the configuration loading process.
refactor(summarization): standardize event names in handlers
Updated event names in the summarization handlers to use constants from GraphEvents for consistency and clarity. This change improves maintainability and reduces the risk of errors related to string literals in event handling.
feat(summarization): enhance usage tracking for summarization events
Added logic to track summarization usage in multiple controllers by checking the current node type. If the node indicates a summarization task, the usage type is set accordingly. This change improves the granularity of usage data collected during summarization processes.
feat(summarization): integrate SummarizationConfig into AppSummarizationConfig type
Enhanced the AppSummarizationConfig type by extending it with the SummarizationConfig type from librechat-data-provider. This change improves type safety and consistency in the summarization configuration structure.
test: add end-to-end tests for summarization functionality
Introduced a comprehensive suite of end-to-end tests for the summarization feature, covering the full LibreChat pipeline from message creation to summarization. This includes a new setup file for environment configuration and a Jest configuration specifically for E2E tests. The tests utilize real API keys and ensure proper integration with the summarization process, enhancing overall test coverage and reliability.
refactor(summarization): include initial summary in formatAgentMessages output
Updated the formatAgentMessages function to return an initial summary alongside messages and index token count map. This change is reflected in multiple controllers and the corresponding tests, enhancing the summarization process by providing additional context for each agent's response.
refactor: move hydrateMissingIndexTokenCounts to tokenMap utility
Extracted the hydrateMissingIndexTokenCounts function from the AgentClient and related tests into a new tokenMap utility file. This change improves code organization and reusability, allowing for better management of token counting logic across the application.
refactor(summarization): standardize step event handling and improve summary rendering
Refactored the step event handling in the useStepHandler and related components to utilize constants for event names, enhancing consistency and maintainability. Additionally, improved the rendering logic in the Summary component to conditionally display the summary text based on its availability, providing a better user experience during the summarization process.
feat(summarization): introduce baseContextTokens and reserveTokensRatio for improved context management
Added baseContextTokens to the InitializedAgent type to calculate the context budget based on agentMaxContextNum and maxOutputTokensNum. Implemented reserveTokensRatio in the createRun function to allow configurable context token management. Updated related tests to validate these changes and ensure proper functionality.
feat(summarization): add minReserveTokens, context pruning, and overflow recovery configurations
Introduced new configuration options for summarization, including minReserveTokens, context pruning settings, and overflow recovery parameters. Updated the createRun function to accommodate these new options and added a comprehensive test suite to validate their functionality and integration within the summarization process.
feat(summarization): add updatePrompt and reserveTokensRatio to summarization configuration
Introduced an updatePrompt field for updating existing summaries with new messages, enhancing the flexibility of the summarization process. Additionally, added reserveTokensRatio to the configuration schema, allowing for improved management of token allocation during summarization. Updated related tests to validate these new features.
feat(logging): add on_agent_log event handler for structured logging
Implemented an on_agent_log event handler in both the agents' callbacks and responses to facilitate structured logging of agent activities. This enhancement allows for better tracking and debugging of agent interactions by logging messages with associated metadata. Updated the summarization process to ensure proper handling of log events.
fix: remove duplicate IBalanceUpdate interface declaration
perf(usage): single-pass partition of collectedUsage
Replace two Array.filter() passes with a single for-of loop that
partitions message vs. summarization usages in one iteration.
fix(BaseClient): shallow-copy message content before mutating and preserve string content
Avoid mutating the original message.content array in-place when
appending a summary block. Also convert string content to a text
content part instead of silently discarding it.
fix(ui): fix Part.tsx indentation and useStepHandler summarize-complete handling
- Fix SUMMARY else-if branch indentation in Part.tsx to match chain level
- Guard ON_SUMMARIZE_COMPLETE with didFinalize flag to avoid unnecessary
re-renders when no summarizing parts exist
- Protect against undefined completeData.summary instead of unsafe spread
fix(agents): use strict enabled check for summarization handlers
Change summarizationConfig?.enabled !== false to === true so handlers
are not registered when summarizationConfig is undefined.
chore: fix initializeClient JSDoc and move DEFAULT_RESERVE_RATIO to module scope
refactor(Summary): align collapse/expand behavior with Reasoning component
- Single render path instead of separate streaming vs completed branches
- Use useMessageContext for isSubmitting/isLatestMessage awareness so
the "Summarizing..." label only shows during active streaming
- Default to collapsed (matching Reasoning), user toggles to expand
- Add proper aria attributes (aria-hidden, role, aria-controls, contentId)
- Hide copy button while actively streaming
feat(summarization): default to self-summarize using agent's own provider/model
When no summarization config is provided (neither in librechat.yaml nor
on the agent), automatically enable summarization using the agent's own
provider and model. The agents package already provides default prompts,
so no prompt configuration is needed.
Also removes the dead resolveSummarizationLLMConfig in summarize.ts
(and its spec) — run.ts buildAgentContext is the single source of truth
for summarization config resolution. Removes the duplicate
RuntimeSummarizationConfig local type in favor of the canonical
SummarizationConfig from data-provider.
chore: schema and type cleanup for summarization
- Add trigger field to summarizationAgentOverrideSchema so per-agent
trigger overrides in librechat.yaml are not silently stripped by Zod
- Remove unused SummarizationStatus type from runs.ts
- Make AppSummarizationConfig.enabled non-optional to reflect the
invariant that loadSummarizationConfig always sets it
refactor(responses): extract duplicated on_agent_log handler
refactor(run): use agents package types for summarization config
Import SummarizationConfig, ContextPruningConfig, and
OverflowRecoveryConfig from @librechat/agents and use them to
type-check the translation layer in buildAgentContext. This ensures
the config object passed to the agent graph matches what it expects.
- Use `satisfies AgentSummarizationConfig` on the config object
- Cast contextPruningConfig and overflowRecoveryConfig to agents types
- Properly narrow trigger fields from DeepPartial to required shape
feat(config): add maxToolResultChars to base endpoint schema
Add maxToolResultChars to baseEndpointSchema so it can be configured
on any endpoint in librechat.yaml. Resolved during agent initialization
using getProviderConfig's endpoint resolution: custom endpoint config
takes precedence, then the provider-specific endpoint config, then the
shared `all` config.
Passed through to the agents package ToolNode, which uses it to cap
tool result length before it enters the context window. When not
configured, the agents package computes a sensible default from
maxContextTokens.
fix(summarization): forward agent model_parameters in self-summarize default
When no explicit summarization config exists, the self-summarize
default now forwards the agent's model_parameters as the
summarization parameters. This ensures provider-specific settings
(e.g. Bedrock region, credentials, endpoint host) are available
when the agents package constructs the summarization LLM.
fix(agents): register summarization handlers by default
Change the enabled gate from === true to !== false so handlers
register when no explicit summarization config exists. This aligns
with the self-summarize default where summarization is always on
unless explicitly disabled via enabled: false.
refactor(summarization): let agents package inherit clientOptions for self-summarize
Remove model_parameters forwarding from the self-summarize default.
The agents package now reuses the agent's own clientOptions when the
summarization provider matches the agent's provider, inheriting all
provider-specific settings (region, credentials, proxy, etc.)
automatically.
refactor(summarization): use MessageContentComplex[] for summary content
Unify summary content to always use MessageContentComplex[] arrays,
matching the pattern used by on_message_delta. No more string | array
unions — content is always an array of typed blocks ({ type: 'text',
text: '...' } for text, { type: 'reasoning_content', ... } for
reasoning).
Agents package:
- SummaryContentBlock.content: MessageContentComplex[] (was string)
- tokenCount now optional (not sent on deltas)
- Removed reasoning field — reasoning is now a content block type
- streamAndCollect normalizes all chunks to content block arrays
- Delta events pass content blocks directly
LibreChat:
- SummaryContentPart.content: Agents.MessageContentComplex[]
- Updated Part.tsx, Summary.tsx, useStepHandler.ts, BaseClient.js
- Summary.tsx derives display text from content blocks via useMemo
- Aggregator uses simple array spread
refactor(summarization): enhance summary handling and text extraction
- Updated BaseClient.js to improve summary text extraction, accommodating both legacy and new content formats.
- Modified summarization logic to ensure consistent handling of summary content across different message formats.
- Adjusted test cases in summarization.e2e.spec.js to utilize the new summary text extraction method.
- Refined SSE useStepHandler to initialize summary content as an array.
- Updated configuration schema by removing unused minReserveTokens field.
- Cleaned up SummaryContentPart type by removing rangeHash property.
These changes streamline the summarization process and ensure compatibility with various content structures.
refactor(summarization): streamline usage tracking and logging
- Removed direct checks for summarization nodes in ModelEndHandler and replaced them with a dedicated markSummarizationUsage function for better readability and maintainability.
- Updated OpenAIChatCompletionController and responses handlers to utilize the new markSummarizationUsage function for setting usage types.
- Enhanced logging functionality by ensuring the logger correctly handles different log levels.
- Introduced a new useCopyToClipboard hook in the Summary component to encapsulate clipboard copy logic, improving code reusability and clarity.
These changes improve the overall structure and efficiency of the summarization handling and logging processes.
refactor(summarization): update summary content block documentation
- Removed outdated comment regarding the last summary content block in BaseClient.js.
- Added a new comment to clarify the purpose of the findSummaryContentBlock method, ensuring consistency in documentation.
These changes enhance code clarity and maintainability by providing accurate descriptions of the summarization logic.
refactor(summarization): update summary content structure in tests
- Modified the summarization content structure in e2e tests to use an array format for text, aligning with recent changes in summary handling.
- Updated test descriptions to clarify the behavior of context token calculations, ensuring consistency and clarity in the tests.
These changes enhance the accuracy and maintainability of the summarization tests by reflecting the updated content structure.
refactor(summarization): remove legacy E2E test setup and configuration
- Deleted the e2e-setup.js and jest.e2e.config.js files, which contained legacy configurations for E2E tests using real API keys.
- Introduced a new summarization.e2e.ts file that implements comprehensive E2E backend integration tests for the summarization process, utilizing real AI providers and tracking summaries throughout the run.
These changes streamline the testing framework by consolidating E2E tests into a single, more robust file while removing outdated configurations.
refactor(summarization): enhance E2E tests and error handling
- Added a cleanup step to force exit after all tests to manage Redis connections.
- Updated the summarization model to 'claude-haiku-4-5-20251001' for consistency across tests.
- Improved error handling in the processStream function to capture and return processing errors.
- Enhanced logging for cross-run tests and tight context scenarios to provide better insights into test execution.
These changes improve the reliability and clarity of the E2E tests for the summarization process.
refactor(summarization): enhance test coverage for maxContextTokens behavior
- Updated run-summarization.test.ts to include a new test case ensuring that maxContextTokens does not exceed user-defined limits, even when calculated ratios suggest otherwise.
- Modified summarization.e2e.ts to replace legacy UsageMetadata type with a more appropriate type for collectedUsage, improving type safety and clarity in the test setup.
These changes improve the robustness of the summarization tests by validating context token constraints and refining type definitions.
feat(summarization): add comprehensive E2E tests for summarization process
- Introduced a new summarization.e2e.test.ts file that implements extensive end-to-end integration tests for the summarization pipeline, covering the full flow from LibreChat to agents.
- The tests utilize real AI providers and include functionality to track summaries during and between runs.
- Added necessary cleanup steps to manage Redis connections post-tests and ensure proper exit.
These changes enhance the testing framework by providing robust coverage for the summarization process, ensuring reliability and performance under real-world conditions.
fix(service): import logger from winston configuration
- Removed the import statement for logger from '@librechat/data-schemas' and replaced it with an import from '~/config/winston'.
- This change ensures that the logger is correctly sourced from the updated configuration, improving consistency in logging practices across the application.
refactor(summary): simplify Summary component and enhance token display
- Removed the unused `meta` prop from the `SummaryButton` component to streamline its interface.
- Updated the token display logic to use a localized string for better internationalization support.
- Adjusted the rendering of the `meta` information to improve its visibility within the `Summary` component.
These changes enhance the clarity and usability of the Summary component while ensuring better localization practices.
feat(summarization): add maxInputTokens configuration for summarization
- Introduced a new `maxInputTokens` property in the summarization configuration schema to control the amount of conversation context sent to the summarizer, with a default value of 10000.
- Updated the `createRun` function to utilize the new `maxInputTokens` setting, allowing for more flexible summarization based on agent context.
These changes enhance the summarization capabilities by providing better control over input token limits, improving the overall summarization process.
refactor(summarization): simplify maxInputTokens logic in createRun function
- Updated the logic for the `maxInputTokens` property in the `createRun` function to directly use the agent's base context tokens when the resolved summarization configuration does not specify a value.
- This change streamlines the configuration process and enhances clarity in how input token limits are determined for summarization.
These modifications improve the maintainability of the summarization configuration by reducing complexity in the token calculation logic.
feat(summary): enhance Summary component to display meta information
- Updated the SummaryContent component to accept an optional `meta` prop, allowing for additional contextual information to be displayed above the main content.
- Adjusted the rendering logic in the Summary component to utilize the new `meta` prop, improving the visibility of supplementary details.
These changes enhance the user experience by providing more context within the Summary component, making it clearer and more informative.
refactor(summarization): standardize reserveRatio configuration in summarization logic
- Replaced instances of `reserveTokensRatio` with `reserveRatio` in the `createRun` function and related tests to unify the terminology across the codebase.
- Updated the summarization configuration schema to reflect this change, ensuring consistency in how the reserve ratio is defined and utilized.
- Removed the per-agent override logic for summarization configuration, simplifying the overall structure and enhancing clarity.
These modifications improve the maintainability and readability of the summarization logic by standardizing the configuration parameters.
* fix: circular dependency of `~/models`
* chore: update logging scope in agent log handlers
Changed log scope from `[agentus:${data.scope}]` to `[agents:${data.scope}]` in both the callbacks and responses controllers to ensure consistent logging format across the application.
* feat: calibration ratio
* refactor(tests): update summarizationConfig tests to reflect changes in enabled property
Modified tests to check for the new `summarizationEnabled` property instead of the deprecated `enabled` field in the summarization configuration. This change ensures that the tests accurately validate the current configuration structure and behavior of the agents.
* feat(tests): add markSummarizationUsage mock for improved test coverage
Introduced a mock for the markSummarizationUsage function in the responses unit tests to enhance the testing of summarization usage tracking. This addition supports better validation of summarization-related functionalities and ensures comprehensive test coverage for the agents' response handling.
* refactor(tests): simplify event handler setup in createResponse tests
Removed redundant mock implementations for event handlers in the createResponse unit tests, streamlining the setup process. This change enhances test clarity and maintainability while ensuring that the tests continue to validate the correct behavior of usage tracking during on_chat_model_end events.
* refactor(agents): move calibration ratio capture to finally block
Reorganized the logic for capturing the calibration ratio in the AgentClient class to ensure it is executed in the finally block. This change guarantees that the ratio is captured even if the run is aborted, enhancing the reliability of the response message persistence. Removed redundant code and improved clarity in the handling of context metadata.
* refactor(agents): streamline bulk write logic in recordCollectedUsage function
Removed redundant bulk write operations and consolidated document handling in the recordCollectedUsage function. The logic now combines all documents into a single bulk write operation, improving efficiency and reducing error handling complexity. Updated logging to provide consistent error messages for bulk write failures.
* refactor(agents): enhance summarization configuration resolution in createRun function
Streamlined the summarization configuration logic by introducing a base configuration and allowing for overrides from agent-specific settings. This change improves clarity and maintainability, ensuring that the summarization configuration is consistently applied while retaining flexibility for customization. Updated the handling of summarization parameters to ensure proper integration with the agent's model and provider settings.
* refactor(agents): remove unused tokenCountMap and streamline calibration ratio handling
Eliminated the unused tokenCountMap variable from the AgentClient class to enhance code clarity. Additionally, streamlined the logic for capturing the calibration ratio by using optional chaining and a fallback value, ensuring that context metadata is consistently defined. This change improves maintainability and reduces potential confusion in the codebase.
* refactor(agents): extract agent log handler for improved clarity and reusability
Refactored the agent log handling logic by extracting it into a dedicated function, `agentLogHandler`, enhancing code clarity and reusability across different modules. Updated the event handlers in both the OpenAI and responses controllers to utilize the new handler, ensuring consistent logging behavior throughout the application.
* test: add summarization event tests for useStepHandler
Implemented a series of tests for the summarization events in the useStepHandler hook. The tests cover scenarios for ON_SUMMARIZE_START, ON_SUMMARIZE_DELTA, and ON_SUMMARIZE_COMPLETE events, ensuring proper handling of summarization logic, including message accumulation and finalization. This addition enhances test coverage and validates the correct behavior of the summarization process within the application.
* refactor(config): update summarizationTriggerSchema to use enum for type validation
Changed the type of the `type` field in the summarizationTriggerSchema from a string to an enum with a single value 'token_count'. This modification enhances type safety and ensures that only valid types are accepted in the configuration, improving overall clarity and maintainability of the schema.
* test(usage): add bulk write tests for message and summarization usage
Implemented tests for the bulk write functionality in the recordCollectedUsage function, covering scenarios for combined message and summarization usage, summarization-only usage, and message-only usage. These tests ensure correct document handling and token rollup calculations, enhancing test coverage and validating the behavior of the usage tracking logic.
* refactor(Chat): enhance clipboard copy functionality and type definitions in Summary component
Updated the Summary component to improve the clipboard copy functionality by handling clipboard permission errors. Refactored type definitions for SummaryProps to use a more specific type, enhancing type safety. Adjusted the SummaryButton and FloatingSummaryBar components to accept isCopied and onCopy props, promoting better separation of concerns and reusability.
* chore(translations): remove unused "Expand Summary" key from English translations
Deleted the "Expand Summary" key from the English translation file to streamline the localization resources and improve clarity in the user interface. This change helps maintain an organized and efficient translation structure.
* refactor: adjust token counting for Claude model to account for API discrepancies
Implemented a correction factor for token counting when using the Claude model, addressing discrepancies between Anthropic's API and local tokenizer results. This change ensures accurate token counts by applying a scaling factor, improving the reliability of token-related functionalities.
* refactor(agents): implement token count adjustment for Claude model messages
Added a method to adjust token counts for messages processed by the Claude model, applying a correction factor to align with API expectations. This enhancement improves the accuracy of token counting, ensuring reliable functionality when interacting with the Claude model.
* refactor(agents): token counting for media content in messages
Introduced a new method to estimate token costs for image and document blocks in messages, improving the accuracy of token counting. This enhancement ensures that media content is properly accounted for, particularly for the Claude model, by integrating additional token estimation logic for various content types. Updated the token counting function to utilize this new method, enhancing overall reliability and functionality.
* chore: fix missing import
* fix(agents): clamp baseContextTokens and document reserve ratio change
Prevent negative baseContextTokens when maxOutputTokens exceeds the
context window (misconfigured models). Document the 10%→5% default
reserve ratio reduction introduced alongside summarization.
* fix(agents): include media tokens in hydrated token counts
Add estimateMediaTokensForMessage to createTokenCounter so the hydration
path (used by hydrateMissingIndexTokenCounts) matches the precomputed
path in AgentClient.getTokenCountForMessage. Without this, messages
containing images or documents were systematically undercounted during
hydration, risking context window overflow.
Add 34 unit tests covering all block-type branches of
estimateMediaTokensForMessage.
* fix(agents): include summarization output tokens in usage return value
The returned output_tokens from recordCollectedUsage now reflects all
billed LLM calls (message + summarization). Previously, summarization
completions were billed but excluded from the returned metadata, causing
a discrepancy between what users were charged and what the response
message reported.
* fix(tests): replace process.exit with proper Redis cleanup in e2e test
The summarization E2E test used process.exit(0) to work around a Redis
connection opened at import time, which killed the Jest runner and
bypassed teardown. Use ioredisClient.quit() and keyvRedisClient.disconnect()
for graceful cleanup instead.
* fix(tests): update getConvo imports in OpenAI and response tests
Refactor test files to import getConvo from the main models module instead of the Conversation submodule. This change ensures consistency across tests and simplifies the import structure, enhancing maintainability.
* fix(clients): improve summary text validation in BaseClient
Refactor the summary extraction logic to ensure that only non-empty summary texts are considered valid. This change enhances the robustness of the message processing by utilizing a dedicated method for summary text retrieval, improving overall reliability.
* fix(config): replace z.any() with explicit union in summarization schema
Model parameters (temperature, top_p, etc.) are constrained to
primitive types rather than the policy-violating z.any().
* refactor(agents): deduplicate CLAUDE_TOKEN_CORRECTION constant
Export from the TS source in packages/api and import in the JS client,
eliminating the static class property that could drift out of sync.
* refactor(agents): eliminate duplicate selfProvider in buildAgentContext
selfProvider and provider were derived from the same expression with
different type casts. Consolidated to a single provider variable.
* refactor(agents): extract shared SSE handlers and restrict log levels
- buildSummarizationHandlers() factory replaces triplicated handler
blocks across responses.js and openai.js
- agentLogHandlerObj exported from callbacks.js for consistent reuse
- agentLogHandler restricted to an allowlist of safe log levels
(debug, info, warn, error) instead of accepting arbitrary strings
* fix(SSE): batch summarize deltas, add exhaustiveness check, conditional error announcement
- ON_SUMMARIZE_DELTA coalesces rapid-fire renders via requestAnimationFrame
instead of calling setMessages per chunk
- Exhaustive never-check on TStepEvent catches unhandled variants at
compile time when new StepEvents are added
- ON_SUMMARIZE_COMPLETE error announcement only fires when a summary
part was actually present and removed
* feat(agents): persist instruction overhead in contextMeta and seed across runs
Extend contextMeta with instructionOverhead and toolCount so the
provider-observed instruction overhead is persisted on the response message
and seeded into the pruner on subsequent runs. This enables the pruner to
use a calibrated budget from the first call instead of waiting for a
provider observation, preventing the ratio collapse caused by local
tokenizer overestimating tool schema tokens.
The seeded overhead is only used when encoding and tool count match
between runs, ensuring stale values from different configurations
are discarded.
* test(agents): enhance OpenAI test mocks for summarization handlers
Updated the OpenAI test suite to include additional mock implementations for summarization handlers, including buildSummarizationHandlers, markSummarizationUsage, and agentLogHandlerObj. This improves test coverage and ensures consistent behavior during testing.
* fix(agents): address review findings for summarization v2
Cancel rAF on unmount to prevent stale Recoil writes from dead
component context. Clear orphaned summarizing:true parts when
ON_SUMMARIZE_COMPLETE arrives without a summary payload. Add null
guard and safe spread to agentLogHandler. Handle Anthropic-format
base64 image/* documents in estimateMediaTokensForMessage. Use
role="region" for expandable summary content. Add .describe() to
contextMeta Zod fields. Extract duplicate usage loop into helper.
* refactor: simplify contextMeta to calibrationRatio + encoding only
Remove instructionOverhead and toolCount from cross-run persistence —
instruction tokens change too frequently between runs (prompt edits,
tool changes) for a persisted seed to be reliable. The intra-run
calibration in the pruner still self-corrects via provider observations.
contextMeta now stores only the tokenizer-bias ratio and encoding,
which are stable across instruction changes.
* test(SSE): enhance useStepHandler tests for ON_SUMMARIZE_COMPLETE behavior
Updated the test for ON_SUMMARIZE_COMPLETE to clarify that it finalizes the existing part with summarizing set to false when the summary is undefined. Added assertions to verify the correct behavior of message updates and the state of summary parts.
* refactor(BaseClient): remove handleContextStrategy and truncateToolCallOutputs functions
Eliminated the handleContextStrategy method from BaseClient to streamline message handling. Also removed the truncateToolCallOutputs function from the prompts module, simplifying the codebase and improving maintainability.
* refactor: add AGENT_DEBUG_LOGGING option and refactor token count handling in BaseClient
Introduced AGENT_DEBUG_LOGGING to .env.example for enhanced debugging capabilities. Refactored token count handling in BaseClient by removing the handleTokenCountMap method and simplifying token count updates. Updated AgentClient to log detailed token count recalculations and adjustments, improving traceability during message processing.
* chore: update dependencies in package-lock.json and package.json files
Bumped versions of several dependencies, including @librechat/agents to ^3.1.62 and various AWS SDK packages to their latest versions. This ensures compatibility and incorporates the latest features and fixes.
* chore: imports order
* refactor: extract summarization config resolution from buildAgentContext
* refactor: rename and simplify summarization configuration shaping function
* refactor: replace AgentClient token counting methods with single-pass pure utility
Extract getTokenCount() and getTokenCountForMessage() from AgentClient
into countFormattedMessageTokens(), a pure function in packages/api that
handles text, tool_call, image, and document content types in one loop.
- Decompose estimateMediaTokensForMessage into block-level helpers
(estimateImageDataTokens, estimateImageBlockTokens, estimateDocumentBlockTokens)
shared by both estimateMediaTokensForMessage and the new single-pass function
- Remove redundant per-call getEncoding() resolution (closure captures once)
- Remove deprecated gpt-3.5-turbo-0301 model branching
- Drop this.getTokenCount guard from BaseClient.sendMessage
* refactor: streamline token counting in createTokenCounter function
Simplified the createTokenCounter function by removing the media token estimation and directly calculating the token count. This change enhances clarity and performance by consolidating the token counting logic into a single pass, while maintaining compatibility with Claude's token correction.
* refactor: simplify summarization configuration types
Removed the AppSummarizationConfig type and directly used SummarizationConfig in the AppConfig interface. This change streamlines the type definitions and enhances consistency across the codebase.
* chore: import order
* fix: summarization event handling in useStepHandler
- Cancel pending summarizeDeltaRaf in clearStepMaps to prevent stale
frames firing after map reset or component unmount
- Move announcePolite('summarize_completed') inside the didFinalize
guard so screen readers only announce when finalization actually occurs
- Remove dead cleanup closure returned from stepHandler useCallback body
that was never invoked by any caller
* fix: estimate tokens for non-PDF/non-image base64 document blocks
Previously estimateDocumentBlockTokens returned 0 for unrecognized MIME
types (e.g. text/plain, application/json), silently underestimating
context budget. Fall back to character-based heuristic or countTokens.
* refactor: return cloned usage from markSummarizationUsage
Avoid mutating LangChain's internal usage_metadata object by returning
a shallow clone with the usage_type tag. Update all call sites in
callbacks, openai, and responses controllers to use the returned value.
* refactor: consolidate debug logging loops in buildMessages
Merge the two sequential O(n) debug-logging passes over orderedMessages
into a single pass inside the map callback where all data is available.
* refactor: narrow SummaryContentPart.content type
Replace broad Agents.MessageContentComplex[] with the specific
Array<{ type: ContentTypes.TEXT; text: string }> that all producers
and consumers already use, improving compile-time safety.
* refactor: use single output array in recordCollectedUsage
Have processUsageGroup append to a shared array instead of returning
separate arrays that are spread into a third, reducing allocations.
* refactor: use for...in in hydrateMissingIndexTokenCounts
Replace Object.entries with for...in to avoid allocating an
intermediate tuple array during token map hydration.
- Deleted the Action test suite located in `api/models/Action.spec.js` to streamline the codebase.
- Updated various test files to reflect changes in model mocks, consolidating mock implementations for user-related actions and enhancing clarity.
- Improved consistency in test setups by aligning with the latest model updates and removing redundant mock definitions.
- Updated imports for `createAgent` and `getAgent` to streamline access from a unified `~/models` path.
- Enhanced test files to reflect the new import structure, ensuring consistency and maintainability across the codebase.
- Improved clarity by removing redundant imports and aligning with the latest model updates.
* Migrate S3 storage module with unit and integration tests
- Migrate S3 CRUD and image operations to packages/api/src/storage/s3/
- Add S3ImageService class with dependency injection
- Add unit tests using aws-sdk-client-mock
- Add integration tests with real s3 bucket (condition presence of AWS_TEST_BUCKET_NAME)
* AI Review Findings Fixes
* chore: tests and refactor S3 storage types
- Added mock implementations for the 'sharp' library in various test files to improve image processing testing.
- Updated type references in S3 storage files from MongoFile to TFile for consistency and type safety.
- Refactored S3 CRUD operations to ensure proper handling of file types and improve code clarity.
- Enhanced integration tests to validate S3 file operations and error handling more effectively.
* chore: rename test file
* Remove duplicate import of refreshS3Url
* chore: imports order
* fix: remove duplicate imports for S3 URL handling in UserController
* fix: remove duplicate import of refreshS3FileUrls in files.js
* test: Add mock implementations for 'sharp' and '@librechat/api' in UserController tests
- Introduced mock functions for the 'sharp' library to facilitate image processing tests, including metadata retrieval and buffer conversion.
- Enhanced mocking for '@librechat/api' to ensure consistent behavior in tests, particularly for the needsRefresh and getNewS3URL functions.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* chore: imports
* chore: optional chaining in `spendTokens.spec.ts`
* feat: Add tenantId field to all MongoDB schemas for multi-tenant isolation
- Add AsyncLocalStorage-based tenant context (`tenantContext.ts`) for
request-scoped tenantId propagation without modifying method signatures
- Add Mongoose `applyTenantIsolation` plugin that injects `{ tenantId }`
into all query filters when tenant context is present, with
`TENANT_ISOLATION_STRICT` env var for fail-closed production mode
- Add optional `tenantId` field to all 28 collection schemas
- Update all compound unique indexes to include tenantId (email, OAuth IDs,
role names, serverName, conversationId+user, messageId+user, etc.)
- Apply tenant isolation plugin in all 28 model factories
- Add `tenantId?: string` to all TypeScript document interfaces
Behaviorally inert — transitional mode (default) passes through all queries
unchanged. No migration required for existing deployments.
* refactor: Update tenant context and enhance tenant isolation plugin
- Changed `tenantId` in `TenantContext` to be optional, allowing for more flexible usage.
- Refactored `runAsSystem` function to accept synchronous functions, improving usability.
- Introduced comprehensive tests for the `applyTenantIsolation` plugin, ensuring correct tenant filtering in various query scenarios.
- Enhanced the plugin to handle aggregate queries and save operations with tenant context, improving data isolation capabilities.
* docs: tenant context documentation and improve tenant isolation tests
- Added detailed documentation for the `tenantStorage` AsyncLocalStorage instance in `tenantContext.ts`, clarifying its usage for async tenant context propagation.
- Updated tests in `tenantIsolation.spec.ts` to improve clarity and coverage, including new tests for strict mode behavior and tenant context propagation through await boundaries.
- Refactored existing test cases for better readability and consistency, ensuring robust validation of tenant isolation functionality.
* feat: Enhance tenant isolation by preventing tenantId mutations in update operations
- Added a new function to assert that tenantId cannot be modified through update operators in Mongoose queries.
- Implemented middleware to enforce this restriction during findOneAndUpdate, updateOne, and updateMany operations.
- Updated documentation to reflect the new behavior regarding tenantId modifications, ensuring clarity on tenant isolation rules.
* feat: Enhance tenant isolation tests and enforce tenantId restrictions
- Updated existing tests to clarify behavior regarding tenantId preservation during save and insertMany operations.
- Introduced new tests to validate that tenantId cannot be modified through update operations, ensuring strict adherence to tenant isolation rules.
- Added checks for mismatched tenantId scenarios, reinforcing the integrity of tenant context propagation.
- Enhanced test coverage for async context propagation and mutation guards, improving overall robustness of tenant isolation functionality.
* fix: Remove duplicate re-exports in utils/index.ts
Merge artifact caused `string` and `tempChatRetention` to be exported
twice, which produces TypeScript compile errors for duplicate bindings.
* fix: Resolve admin capability gap in multi-tenant mode (TODO #12091)
- hasCapabilityForPrincipals now queries both tenant-scoped AND
platform-level grants when tenantId is set, so seeded ADMIN grants
remain effective in tenant mode.
- Add applyTenantIsolation to SystemGrant model factory.
* fix: Harden tenant isolation plugin
- Add replaceGuard for replaceOne/findOneAndReplace to prevent
cross-tenant document reassignment via replacement documents.
- Cache isStrict() result to avoid process.env reads on every query.
Export _resetStrictCache() for test teardown.
- Replace console.warn with project logger (winston).
- Add 5 new tests for replace guard behavior (46 total).
* style: Fix import ordering in convo.ts and message.ts
Move type imports after value imports per project style guide.
* fix: Remove tenant isolation from SystemGrant, stamp tenantId in replaceGuard
- SystemGrant is a cross-tenant control plane whose methods handle
tenantId conditions explicitly. Applying the isolation plugin
injects a hard equality filter that overrides the $and/$or logic
in hasCapabilityForPrincipals, making platform-level ADMIN grants
invisible in tenant mode.
- replaceGuard now stamps tenantId into replacement documents when
absent, preventing replaceOne from silently stripping tenant
context. Replacements with a matching tenantId are allowed;
mismatched tenantId still throws.
* test: Add multi-tenant unique constraint and replace stamping tests
- Verify same name/email can exist in different tenants (compound
unique index allows it).
- Verify duplicate within same tenant is rejected (E11000).
- Verify tenant-scoped query returns only the correct document.
- Update replaceOne test to assert tenantId is stamped into
replacement document.
- Add test for replacement with matching tenantId.
* style: Reorder imports in message.ts to align with project style guide
* feat: Add migration to drop superseded unique indexes for multi-tenancy
Existing deployments have single-field unique indexes (e.g. { email: 1 })
that block multi-tenant operation — same email in different tenants
triggers E11000. Mongoose autoIndex creates the new compound indexes
but never drops the old ones.
dropSupersededTenantIndexes() drops all 19 superseded indexes across 11
collections. It is idempotent, skips missing indexes/collections, and
is a no-op on fresh databases.
Must be called before enabling multi-tenant middleware on an existing
deployment. Single-tenant deployments are unaffected (old indexes
coexist harmlessly until migration runs).
Includes 11 tests covering:
- Full upgrade simulation (create old indexes, drop them, verify gone)
- Multi-tenant writes work after migration (same email, different tenant)
- Intra-tenant uniqueness preserved (duplicate within tenant rejected)
- Fresh database (no-op, no errors)
- Partial migration (some collections exist, some don't)
- SUPERSEDED_INDEXES coverage validation
* fix: Update systemGrant test — platform grants now satisfy tenant queries
The TODO #12091 fix intentionally changed hasCapabilityForPrincipals to
match both tenant-scoped AND platform-level grants. The test expected
the old behavior (platform grant invisible to tenant query). Updated
test name and expectation to match the new semantics.
* fix: Align getCapabilitiesForPrincipal with hasCapabilityForPrincipals tenant query
getCapabilitiesForPrincipal used a hard tenantId equality filter while
hasCapabilityForPrincipals uses $and/$or to match both tenant-scoped
and platform-level grants. This caused the two functions to disagree
on what grants a principal holds in tenant mode.
Apply the same $or pattern: when tenantId is provided, match both
{ tenantId } and { tenantId: { $exists: false } }.
Adds test verifying platform-level ADMIN grants appear in
getCapabilitiesForPrincipal when called with a tenantId.
* fix: Remove categories from tenant index migration
categoriesSchema is exported but never used to create a Mongoose model.
No Category model factory exists, no code constructs a model from it,
and no categories collection exists in production databases. Including
it in the migration would attempt to drop indexes from a non-existent
collection (harmlessly skipped) but implies the collection is managed.
* fix: Restrict runAsSystem to async callbacks only
Sync callbacks returning Mongoose thenables silently lose ALS context —
the system bypass does nothing and strict mode throws with no indication
runAsSystem was involved. Narrowing to () => Promise<T> makes the wrong
pattern a compile error. All existing call sites already use async.
* fix: Use next(err) consistently in insertMany pre-hook
The hook accepted a next callback but used throw for errors. Standardize
on next(err) for all error paths so the hook speaks one language —
callback-style throughout.
* fix: Replace optional chaining with explicit null assertions in spendTokens tests
Optional chaining on test assertions masks failures with unintelligible
error messages. Add expect(result).not.toBeNull() before accessing
properties, so a null result produces a clear diagnosis instead of
"received value must be a number".
* feat: Implement System Grants for Role-Based Capabilities
- Added a new `systemGrant` model and associated methods to manage role-based capabilities within the application.
- Introduced middleware functions `hasCapability` and `requireCapability` to check user permissions based on their roles.
- Updated the database seeding process to include system grants for the ADMIN role, ensuring all necessary capabilities are assigned on startup.
- Enhanced type definitions and schemas to support the new system grant functionality, improving overall type safety and clarity in the codebase.
* test: Add unit tests for capabilities middleware and system grant methods
- Introduced comprehensive unit tests for the capabilities middleware, including `hasCapability` and `requireCapability`, ensuring proper permission checks based on user roles.
- Added tests for the `SystemGrant` methods, verifying the seeding of system grants, capability granting, and revocation processes.
- Enhanced test coverage for edge cases, including idempotency of grant operations and handling of unexpected errors in middleware.
- Utilized mocks for database interactions to isolate tests and improve reliability.
* refactor: Transition to Capability-Based Access Control
- Replaced role-based access checks with capability-based checks across various middleware and routes, enhancing permission management.
- Introduced `hasCapability` and `requireCapability` functions to streamline capability verification for user actions.
- Updated relevant routes and middleware to utilize the new capability system, ensuring consistent permission enforcement.
- Enhanced type definitions and added tests for the new capability functions, improving overall code reliability and maintainability.
* test: Enhance capability-based access tests for ADMIN role
- Updated tests to reflect the new capability-based access control, specifically for the ADMIN role.
- Modified test descriptions to clarify that users with the MANAGE_AGENTS capability can bypass permission checks.
- Seeded capabilities for the ADMIN role in multiple test files to ensure consistent permission checks across different routes and middleware.
- Improved overall test coverage for capability verification, ensuring robust permission management.
* test: Update capability tests for MCP server access
- Renamed test to reflect the correct capability for bypassing permission checks, changing from MANAGE_AGENTS to MANAGE_MCP_SERVERS.
- Updated seeding of capabilities for the ADMIN role to align with the new capability structure.
- Ensured consistency in capability definitions across tests and middleware for improved permission management.
* feat: Add hasConfigCapability for enhanced config access control
- Introduced `hasConfigCapability` function to check user permissions for managing or reading specific config sections.
- Updated middleware to export the new capability function, ensuring consistent access control across the application.
- Enhanced unit tests to cover various scenarios for the new capability, improving overall test coverage and reliability.
* fix: Update tenantId filter in createSystemGrantMethods
- Added a condition to set tenantId filter to { $exists: false } when tenantId is null, ensuring proper handling of cases where tenantId is not provided.
- This change improves the robustness of the system grant methods by explicitly managing the absence of tenantId in the filter logic.
* fix: account deletion capability check
- Updated the `canDeleteAccount` middleware to ensure that the `hasManageUsers` capability check only occurs if a user is present, preventing potential errors when the user object is undefined.
- This change improves the robustness of the account deletion logic by ensuring proper handling of user permissions.
* refactor: Optimize seeding of system grants for ADMIN role
- Replaced sequential capability granting with parallel execution using Promise.all in the seedSystemGrants function.
- This change improves performance and efficiency during the initialization of system grants, ensuring all capabilities are granted concurrently.
* refactor: Simplify systemGrantSchema index definition
- Removed the sparse option from the unique index on principalType, principalId, capability, and tenantId in the systemGrantSchema.
- This change streamlines the index definition, potentially improving query performance and clarity in the schema design.
* refactor: Reorganize role capability check in roles route
- Moved the capability check for reading roles to occur after parsing the roleName, improving code clarity and structure.
- This change ensures that the authorization logic is consistently applied before fetching role details, enhancing overall permission management.
* refactor: Remove unused ISystemGrant interface from systemCapabilities.ts
- Deleted the ISystemGrant interface as it was no longer needed, streamlining the code and improving clarity.
- This change helps reduce clutter in the file and focuses on relevant capabilities for the system.
* refactor: Migrate SystemCapabilities to data-schemas
- Replaced imports of SystemCapabilities from 'librechat-data-provider' with imports from '@librechat/data-schemas' across multiple files.
- This change centralizes the management of system capabilities, improving code organization and maintainability.
* refactor: Update account deletion middleware and capability checks
- Modified the `canDeleteAccount` middleware to ensure that the account deletion permission is only granted to users with the `MANAGE_USERS` capability, improving security and clarity in permission management.
- Enhanced error logging for unauthorized account deletion attempts, providing better insights into permission issues.
- Updated the `capabilities.ts` file to ensure consistent handling of user authentication checks, improving robustness in capability verification.
- Refined type definitions in `systemGrant.ts` and `systemGrantMethods.ts` to utilize the `PrincipalType` enum, enhancing type safety and code clarity.
* refactor: Extract principal ID normalization into a separate function
- Introduced `normalizePrincipalId` function to streamline the normalization of principal IDs based on their type, enhancing code clarity and reusability.
- Updated references in `createSystemGrantMethods` to utilize the new normalization function, improving maintainability and reducing code duplication.
* test: Add unit tests for principalId normalization in systemGrant
- Introduced tests for the `grantCapability`, `revokeCapability`, and `getCapabilitiesForPrincipal` methods to verify correct handling of principalId normalization between string and ObjectId formats.
- Enhanced the `capabilities.ts` middleware to utilize the `PrincipalType` enum for improved type safety.
- Added a new utility function `normalizePrincipalId` to streamline principal ID normalization logic, ensuring consistent behavior across the application.
* feat: Introduce capability implications and enhance system grant methods
- Added `CapabilityImplications` to define relationships between broader and implied capabilities, allowing for more intuitive permission checks.
- Updated `createSystemGrantMethods` to expand capability queries to include implied capabilities, improving authorization logic.
- Enhanced `systemGrantSchema` to include an `expiresAt` field for future TTL enforcement of grants, and added validation to ensure `tenantId` is not set to null.
- Documented authorization requirements for prompt group and prompt deletion methods to clarify access control expectations.
* test: Add unit tests for canDeleteAccount middleware
- Introduced unit tests for the `canDeleteAccount` middleware to verify account deletion permissions based on user roles and capabilities.
- Covered scenarios for both allowed and blocked account deletions, including checks for ADMIN users with the `MANAGE_USERS` capability and handling of undefined user cases.
- Enhanced test structure to ensure clarity and maintainability of permission checks in the middleware.
* fix: Add principalType enum validation to SystemGrant schema
Without enum validation, any string value was accepted for principalType
and silently stored. Invalid documents would never match capability
queries, creating phantom grants impossible to diagnose without raw DB
inspection. All other ACL models in the codebase validate this field.
* fix: Replace seedSystemGrants Promise.all with bulkWrite for concurrency safety
When two server instances start simultaneously (K8s rolling deploy, PM2
cluster), both call seedSystemGrants. With Promise.all + findOneAndUpdate
upsert, both instances may attempt to insert the same documents, causing
E11000 duplicate key errors that crash server startup.
bulkWrite with ordered:false handles concurrent upserts gracefully and
reduces 17 individual round trips to a single network call. The returned
documents (previously discarded) are no longer fetched.
* perf: Add AsyncLocalStorage per-request cache for capability checks
Every hasCapability call previously required 2 DB round trips
(getUserPrincipals + SystemGrant.exists) — replacing what were O(1)
string comparisons. Routes like patchPromptGroup triggered this twice,
and hasConfigCapability's fallback path resolved principals twice.
This adds a per-request AsyncLocalStorage cache that:
- Caches resolved principals (same for all checks within one request)
- Caches capability check results (same user+cap = same answer)
- Automatically scoped to request lifetime (no stale grants)
- Falls through to DB when no store exists (background jobs, tests)
- Requires no signature changes to hasCapability
The capabilityContextMiddleware is registered at the app level before
all routes, initializing a fresh store per request.
* fix: Add error handling for inline hasCapability calls
canDeleteAccount, fetchAssistants, and validateAuthor all call
hasCapability without try-catch. These were previously O(1) string
comparisons that could never throw. Now they hit the database and can
fail on connection timeout or transient errors.
Wrap each call in try-catch, defaulting to deny (false) on error.
This ensures a DB hiccup returns a clean 403 instead of an unhandled
500 with a stack trace.
* test: Add canDeleteAccount DB-error resilience test
Tests that hasCapability rejection (e.g., DB timeout) results in a clean
403 rather than an unhandled exception. Validates the error handling
added in the previous commit.
* refactor: Use barrel import for hasCapability in validateAuthor
Import from ~/server/middleware barrel instead of directly from
~/server/middleware/roles/capabilities for consistency with other
non-middleware consumers. Files within the middleware barrel itself
must continue using direct imports to avoid circular requires.
* refactor: Remove misleading pre('save') hook from SystemGrant schema
The pre('save') hook normalized principalId for USER/GROUP principals,
but the primary write path (grantCapability) uses findOneAndUpdate —
which does not trigger save hooks. The normalization was already handled
explicitly in grantCapability itself. The hook created a false impression
of schema-level enforcement that only covered save()/create() paths.
Replace with a comment documenting that all writes must go through
grantCapability.
* feat: Add READ_ASSISTANTS capability to complete manage/read pair
Every other managed resource had a paired READ_X / MANAGE_X capability
except assistants. This adds READ_ASSISTANTS and registers the
MANAGE_ASSISTANTS → READ_ASSISTANTS implication in CapabilityImplications,
enabling future read-only assistant visibility grants.
* chore: Reorder systemGrant methods for clarity
Moved hasCapabilityForPrincipals to a more logical position in the returned object of createSystemGrantMethods, improving code readability. This change also maintains the inclusion of seedSystemGrants in the export, ensuring all necessary methods are available.
* fix: Wrap seedSystemGrants in try-catch to avoid blocking startup
Seeding capabilities is idempotent and will succeed on the next restart.
A transient DB error during seeding should not prevent the server from
starting — log the error and continue.
* refactor: Improve capability check efficiency and add audit logging
Move hasCapability calls after cheap early-exits in validateAuthor and
fetchAssistants so the DB check only runs when its result matters. Add
logger.debug on every capability bypass grant across all 7 call sites
for auditability, and log errors in catch blocks instead of silently
swallowing them.
* test: Add integration tests for AsyncLocalStorage capability caching
Exercises the full vertical — ALS context, generateCapabilityCheck,
real getUserPrincipals, real hasCapabilityForPrincipals, real MongoDB
via MongoMemoryServer. Covers per-request caching, cross-context
isolation, concurrent request isolation, negative caching, capability
implications, tenant scoping, group-based grants, and requireCapability
middleware.
* test: Add systemGrant data-layer and ALS edge-case integration tests
systemGrant.spec.ts (51 tests): Full integration tests for all
systemGrant methods against real MongoDB — grant/revoke lifecycle,
principalId normalization (string→ObjectId for USER/GROUP, string for
ROLE), capability implications (both directions), tenant scoping,
schema validation (null tenantId, invalid enum, required fields,
unique compound index).
capabilities.integration.spec.ts (27 tests): Adds ALS edge cases —
missing context degrades gracefully with no caching (background jobs,
child processes), nested middleware creates independent inner context,
optional-chaining safety when store is undefined, mid-request grant
changes are invisible due to result caching, requireCapability works
without ALS, and interleaved concurrent contexts maintain isolation.
* fix: Add worker thread guards to capability ALS usage
Detect when hasCapability or capabilityContextMiddleware is called from
a worker thread (where ALS context does not propagate from the parent).
hasCapability logs a warn-once per factory instance; the middleware logs
an error since mounting Express middleware in a worker is likely a
misconfiguration. Both continue to function correctly — the guard is
observability, not a hard block.
* fix: Include tenantId in ALS principal cache key for tenant isolation
The principal cache key was user.id:user.role, which would reuse
cached principals across tenants for the same user within a request.
When getUserPrincipals gains tenant-scoped group resolution, principals
from tenant-a would incorrectly serve tenant-b checks. Changed to
user.id:user.role:user.tenantId to prevent cross-tenant cache hits.
Adds integration test proving separate principal lookups per tenantId.
* test: Remove redundant mocked capabilities.spec.js
The JS wrapper test (7 tests, all mocked) is a strict subset of
capabilities.integration.spec.ts (28 tests, real MongoDB). Every
scenario it covered — hasCapability true/false, tenantId passthrough,
requireCapability 403/500, error handling — is tested with higher
fidelity in the integration suite.
* test: Replace mocked canDeleteAccount tests with real MongoDB integration
Remove hasCapability mock — tests now exercise the full capability
chain against real MongoDB (getUserPrincipals, hasCapabilityForPrincipals,
SystemGrant collection). Only mocks remaining are logger and cache.
Adds new coverage: admin role without grant is blocked, user-level
grant bypasses deletion restriction, null user handling.
* test: Add comprehensive tests for ACL entry management and user group methods
Introduces new tests for `deleteAclEntries`, `bulkWriteAclEntries`, and `findPublicResourceIds` in `aclEntry.spec.ts`, ensuring proper functionality for deleting and bulk managing ACL entries. Additionally, enhances `userGroup.spec.ts` with tests for finding groups by ID and name pattern, including external ID matching and source filtering. These changes improve coverage and validate the integrity of ACL and user group operations against real MongoDB interactions.
* refactor: Update capability checks and logging for better clarity and error handling
Replaced `MANAGE_USERS` with `ACCESS_ADMIN` in the `canDeleteAccount` middleware and related tests to align with updated permission structure. Enhanced logging in various middleware functions to use `logger.warn` for capability check failures, providing clearer error messages. Additionally, refactored capability checks in the `patchPromptGroup` and `validateAuthor` functions to improve readability and maintainability. This commit also includes adjustments to the `systemGrant` methods to implement retry logic for transient failures during capability seeding, ensuring robustness in the face of database errors.
* refactor: Enhance logging and retry logic in seedSystemGrants method
Updated the logging format in the seedSystemGrants method to include error messages for better clarity. Improved the retry mechanism by explicitly mocking multiple failures in tests, ensuring robust error handling during transient database issues. Additionally, refined imports in the systemGrant schema for better type management.
* refactor: Consolidate imports in canDeleteAccount middleware
Merged logger and SystemCapabilities imports from the data-schemas module into a single line for improved readability and maintainability of the code. This change streamlines the import statements in the canDeleteAccount middleware.
* test: Enhance systemGrant tests for error handling and capability validation
Added tests to the systemGrant methods to handle various error scenarios, including E11000 race conditions, invalid ObjectId strings for USER and GROUP principals, and invalid capability strings. These enhancements improve the robustness of the capability granting and revoking logic, ensuring proper error propagation and validation of inputs.
* fix: Wrap hasCapability calls in deny-by-default try-catch at remaining sites
canAccessResource, files.js, and roles.js all had hasCapability inside
outer try-catch blocks that returned 500 on DB failure instead of
falling through to the regular ACL check. This contradicts the
deny-by-default pattern used everywhere else.
Also removes raw error.message from the roles.js 500 response to
prevent internal host/connection info leaking to clients.
* fix: Normalize user ID in canDeleteAccount before passing to hasCapability
requireCapability normalizes req.user.id via _id?.toString() fallback,
but canDeleteAccount passed raw req.user directly. If req.user.id is
absent (some auth layers only populate _id), getUserPrincipals received
undefined, silently returning empty principals and blocking the bypass.
* fix: Harden systemGrant schema and type safety
- Reject empty string tenantId in schema validator (was only blocking
null; empty string silently orphaned documents)
- Fix reverseImplications to use BaseSystemCapability[] instead of
string[], preserving the narrow discriminated type
- Document READ_ASSISTANTS as reserved/unenforced
* test: Use fake timers for seedSystemGrants retry tests and add tenantId validation
- Switch retry tests to jest.useFakeTimers() to eliminate 3+ seconds
of real setTimeout delays per test run
- Add regression test for empty-string tenantId rejection
* docs: Add TODO(#12091) comments for tenant-scoped capability gaps
In multi-tenant mode, platform-level grants (no tenantId) won't match
tenant-scoped queries, breaking admin access. getUserPrincipals also
returns cross-tenant group memberships. Both need fixes in #12091.
* 🧹 chore: resolve imports due to rebase
* chore: Update model mocks in unit tests for consistency
- Consolidated model mock implementations across various test files to streamline setup and reduce redundancy.
- Removed duplicate mock definitions for `getMultiplier` and `getCacheMultiplier`, ensuring a unified approach in `recordCollectedUsage.spec.js`, `openai.spec.js`, `responses.unit.spec.js`, and `abortMiddleware.spec.js`.
- Enhanced clarity and maintainability of test files by aligning mock structures with the latest model updates.
* fix: Safeguard token credit checks in transaction tests
- Updated assertions in `transaction.spec.ts` to handle potential null values for `updatedBalance` by using optional chaining.
- Enhanced robustness of tests related to token credit calculations, ensuring they correctly account for scenarios where the balance may not be found.
* chore: transaction methods with bulk insert functionality
- Introduced `bulkInsertTransactions` method in `transaction.ts` to facilitate batch insertion of transaction documents.
- Updated test file `transactions.bulk-parity.spec.ts` to utilize new pricing function assignments and handle potential null values in calculations, improving test robustness.
- Refactored pricing function initialization for clarity and consistency.
* refactor: Enhance type definitions and introduce new utility functions for model matching
- Added `findMatchingPattern` and `matchModelName` utility functions to improve model name matching logic in transaction methods.
- Updated type definitions for `findMatchingPattern` to accept a more specific tokensMap structure, enhancing type safety.
- Refactored `dbMethods` initialization in `transactions.bulk-parity.spec.ts` to include the new utility functions, improving test clarity and functionality.
* refactor: Update database method imports and enhance transaction handling
- Refactored `abortMiddleware.js` to utilize centralized database methods for message handling and conversation retrieval, improving code consistency.
- Enhanced `bulkInsertTransactions` in `transaction.ts` to handle empty document arrays gracefully and added error logging for better debugging.
- Updated type definitions in `transactions.ts` to enforce stricter typing for token types, enhancing type safety across transaction methods.
- Improved test setup in `transactions.bulk-parity.spec.ts` by refining pricing function assignments and ensuring robust handling of potential null values.
* refactor: Update database method references and improve transaction multiplier handling
- Refactored `client.js` to update database method references for `bulkInsertTransactions` and `updateBalance`, ensuring consistency in method usage.
- Enhanced transaction multiplier calculations in `transaction.spec.ts` to provide fallback values for write and read multipliers, improving robustness in cost calculations across structured token spending tests.
* chore: move database model methods to /packages/data-schemas
* chore: add TypeScript ESLint rule to warn on unused variables
* refactor: model imports to streamline access
- Consolidated model imports across various files to improve code organization and reduce redundancy.
- Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path.
- Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact.
- Enhanced test files to align with the new import paths, maintaining test coverage and integrity.
* chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas
* test: update agent model mocks in unit tests
- Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality.
- Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication.
- Ensured consistency in agent mock implementations across test files.
* fix: update types in data-schemas
* refactor: enhance type definitions in transaction and spending methods
- Updated type definitions in `checkBalance.ts` to use specific request and response types.
- Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety.
- Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods.
- Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness.
* refactor: streamline model imports and enhance code organization
- Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy.
- Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact.
- Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods.
* feat: implement loadAddedAgent and refactor agent loading logic
- Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution.
- Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`.
- Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`.
- Enhanced type definitions and improved error handling in the agent loading process.
- Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage.
* refactor: enhance balance handling with new update interface
- Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase.
- Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety.
- Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities.
- Enhanced overall code clarity and maintainability by refining type definitions related to balance operations.
* feat: add unit tests for loadAgent functionality and enhance agent loading logic
- Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks.
- Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality.
- Improved test coverage for agent loading, including handling of non-existent agents and user permissions.
* chore: reorder memory method exports for consistency
- Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`.
* chore: remove projects and projectIds usage
* chore: empty line linting
* chore: remove isCollaborative property across agent models and related tests
- Removed the isCollaborative property from agent models, controllers, and tests, as it is deprecated in favor of ACL permissions.
- Updated related validation schemas and data provider types to reflect this change.
- Ensured all references to isCollaborative were stripped from the codebase to maintain consistency and clarity.
* feat: replace unsupported MongoDB aggregation operators for FerretDB compatibility
Replace $lookup, $unwind, $sample, $replaceRoot, and $addFields aggregation
stages which are unsupported on FerretDB v2.x (postgres-documentdb backend).
- Prompt.js: Replace $lookup/$unwind/$project pipelines with find().select().lean()
+ attachProductionPrompts() batch helper. Replace $group/$replaceRoot/$sample
in getRandomPromptGroups with distinct() + Fisher-Yates shuffle.
- Agent/Prompt migration scripts: Replace $lookup anti-join pattern with
distinct() + $nin two-step queries for finding un-migrated resources.
All replacement patterns verified against FerretDB v2.7.0.
* fix: use $pullAll for simple array removals, fix memberIds type mismatches
Replace $pull with $pullAll for exact-value scalar array removals. Both
operators work on MongoDB and FerretDB, but $pullAll is more explicit for
exact matching (no condition expressions).
Fix critical type mismatch bugs where ObjectId values were used against
String[] memberIds arrays in Group queries:
- config/delete-user.js: use string uid instead of ObjectId user._id
- e2e/setup/cleanupUser.ts: convert userId.toString() before query
Harden PermissionService.bulkUpdateResourcePermissions abort handling to
prevent crash when abortTransaction is called after commitTransaction.
All changes verified against FerretDB v2.7.0 and MongoDB Memory Server.
* fix: harden transaction support probe for FerretDB compatibility
Commit the transaction before aborting in supportsTransactions probe, and
wrap abortTransaction in try-catch to prevent crashes when abort is called
after a successful commit (observed behavior on FerretDB).
* feat: add FerretDB compatibility test suite, retry utilities, and CI config
Add comprehensive FerretDB integration test suite covering:
- $pullAll scalar array operations
- $pull with subdocument conditions
- $lookup replacement (find + manual join)
- $sample replacement (distinct + Fisher-Yates)
- $bit and $bitsAllSet operations
- Migration anti-join pattern
- Multi-tenancy (useDb, scaling, write amplification)
- Sharding proof-of-concept
- Production operations (backup/restore, schema migration, deadlock retry)
Add production retryWithBackoff utility for deadlock recovery during
concurrent index creation on FerretDB/DocumentDB backends.
Add UserController.spec.js tests for deleteUserController (runs in CI).
Configure jest and eslint to isolate FerretDB tests from CI pipelines:
- packages/data-schemas/jest.config.mjs: ignore misc/ directory
- eslint.config.mjs: ignore packages/data-schemas/misc/
Include Docker Compose config for local FerretDB v2.7 + postgres-documentdb,
dedicated jest/tsconfig for the test files, and multi-tenancy findings doc.
* style: brace formatting in aclEntry.ts modifyPermissionBits
* refactor: reorganize retry utilities and update imports
- Moved retryWithBackoff utility to a new file `retry.ts` for better structure.
- Updated imports in `orgOperations.ferretdb.spec.ts` to reflect the new location of retry utilities.
- Removed old import statement for retryWithBackoff from index.ts to streamline exports.
* test: add $pullAll coverage for ConversationTag and PermissionService
Add integration tests for deleteConversationTag verifying $pullAll
removes tags from conversations correctly, and for
syncUserEntraGroupMemberships verifying $pullAll removes user from
non-matching Entra groups while preserving local group membership.
---------
* fix(mcp): pass missing customUserVars and user during unauthenticated tool discovery
* fix(mcp): type-safe user context forwarding for non-OAuth tool discovery
Extract UserConnectionContext from OAuthConnectionOptions to properly
model the non-OAuth case where user/customUserVars/requestBody need
placeholder resolution without requiring OAuth-specific fields.
- Remove prohibited `as unknown as` double-cast
- Forward requestBody and connectionTimeout (previously omitted)
- Add unit tests for argument forwarding at Manager and Factory layers
- Add integration test exercising real processMCPEnv substitution
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
Add dispatch-to-universe job to docker-publish workflow so universe
can trigger staging deploy, E2E tests, and production promotion
after each image push.
* 🐛 fix: Gate MCP queries behind USE permission to prevent 403 spam
Closes#12342
When `interface.mcpServers.use` is set to `false` in `librechat.yaml`,
the frontend was still unconditionally fetching `/api/mcp/servers` on
every app startup, window focus, and stale interval — producing
continuous 403 "Insufficient permissions" log entries.
Add `useHasAccess` permission checks to both `useMCPServersQuery` call
sites (`useAppStartup` and `useMCPServerManager`) so the query is
disabled when the user lacks `MCP_SERVERS.USE`, matching the guard
pattern already used by MCP UI components.
* fix: Lint and import order corrections
* fix: Address review findings — gate permissions query, add tests
- Gate `useGetAllEffectivePermissionsQuery` behind `canUseMcp` in
`useMCPServerManager` for consistency (wasted request when MCP
disabled, even though this endpoint doesn't 403)
- Sort multi-line `librechat-data-provider` import shortest to longest
- Restore intent comment on `useGetStartupConfig` call
- Add `useAppStartup` test suite covering MCP permission gating:
query suppression when USE denied, compound `enabled` conditions
for tools query (servers loading, empty, no user)
* fix: pass along error message when OCR fails
Right now, if OCR fails, it just says "Error processing file" which
isn't very helpful.
The `error.message` does has helpful information in it, but our
filter wasn't including the right case to pass it along. Now it does!
* fix: extract shared upload error filter, apply to images route
The 'Unable to extract text from' error was only allowlisted in the
files route but not the images route, which also calls
processAgentFileUpload. Extract the duplicated error filter logic
into a shared resolveUploadErrorMessage utility in packages/api so
both routes stay in sync.
---------
Co-authored-by: Dan Lew <daniel@mightyacorn.com>
* fix: add screen reader-only context for convo date groupings
Otherwise, the screen reader simply says something like "today" or
"previous 7 days" without any other context, which is confusing (especially
since this is a heading, so theoretically something you'd navigate to
directly).
Visually it's identical to before, but screen readers have added context
now.
* fix: move a11y key to com_a11y_* namespace and add DateLabel test
Move screen-reader-only translation key from com_ui_* to com_a11y_*
namespace where it belongs, and add test coverage to prevent silent
accessibility regressions.
---------
Co-authored-by: Dan Lew <daniel@mightyacorn.com>
* fix: distinguish message headings for screen readers
Before, each message would have the heading of either the name of
the user or the name of the agent (e.g. "Dan Lew" or "Claude Sonnet").
If you tried to navigate that with a screen reader, you'd just see
a ton of headings switching back and forth between the two with no
way to figure out where in the conversation each is.
Now, we prefix each header with whether it's a "prompt" or "response",
plus we number them so that you can distinguish how far in the
conversation each part is.
(This is a screen reader only change - there's no visual difference.)
* fix: patch MessageParts heading, guard negative depth, add tests
- Add sr-only heading prefix to MessageParts.tsx (Assistants endpoint path)
- Extract shared getMessageNumber helper to avoid DRY violation between
getMessageAriaLabel and getHeaderPrefixForScreenReader
- Guard against depth < 0 producing "Prompt 0:" / "Response 0:"
- Remove unused lodash import
- Add unit tests covering all branches including depth edge cases
---------
Co-authored-by: Dan Lew <daniel@mightyacorn.com>
The react-markdown dependency chain uses Node.js subpath imports
(vfile/lib/#minpath) that Sandpack's bundler cannot resolve, breaking
markdown artifact preview. Switch to a self-contained static HTML page
using marked.js from CDN, eliminating the React bootstrap overhead and
the problematic dependency resolution.
* docs: add Simplified Chinese translation (README.zh.md)
* docs: sync README.zh.md with current English README
Address review findings:
- Fix stale Railway URLs (railway.app -> railway.com, /template/ -> /deploy/)
- Add missing Resumable Streams section
- Add missing sub-features (Agent Marketplace, Collaborative Sharing, Jina
Reranking, prompt sharing, Helicone provider)
- Update multilingual UI language list from 18 to 30+ languages
- Replace outdated "ChatGPT clone" platform description with current messaging
- Remove stale video embed no longer in English README
- Remove non-standard bold wrappers on links
- Fix trailing whitespace
- Add sync header with date and commit SHA
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* allow smtp server that does not have authentication
* fix: align checkEmailConfig with optional SMTP credentials and add tests
Remove EMAIL_USERNAME/EMAIL_PASSWORD requirements from the hasSMTPConfig
predicate in checkEmailConfig() so the rest of the codebase (login,
startup checks, invite-user) correctly recognizes unauthenticated SMTP
as a valid email configuration.
Add a warning when only one of the two credential env vars is set,
in both sendEmail.js and checkEmailConfig(), to catch partial
misconfigurations early.
Add test coverage for both the transporter auth assembly in sendEmail.js
and the checkEmailConfig predicate in packages/api.
Document in .env.example that credentials are optional for
unauthenticated SMTP relays.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: Exclude memory directory from gitignore for API package
* fix: Scope memory/ and coordination/ gitignore to repo root
Prefix patterns with `/` so they only match root-level Claude Flow
artifact directories, not workspace source like packages/api/src/memory/.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: preserve ban data object in checkBan to prevent permanent cache
The !! operator on line 108 coerces the ban data object to a boolean,
losing the expiresAt property. This causes:
1. Number(true.expiresAt) = NaN → expired bans never cleaned from banLogs
2. banCache.set(key, true, NaN) → Keyv stores with expires: null (permanent)
3. IP-based cache entries persist indefinitely, blocking unrelated users
Fix: replace isBanned (boolean) with banData (original object) so that
expiresAt is accessible for TTL calculation and proper cache expiry.
* fix: address checkBan cleanup defects exposed by ban data fix
The prior commit correctly replaced boolean coercion with the ban data
object, but activated previously-dead cleanup code with several defects:
- IP-only expired bans fell through cleanup without returning next(),
re-caching with negative TTL (permanent entry) and blocking the user
- Redis deployments used cache-prefixed keys for banLogs.delete(),
silently failing since bans are stored at raw keys
- banCache.set() calls were fire-and-forget, silently dropping errors
- No guard for missing/invalid expiresAt reproduced the NaN TTL bug
on legacy ban records
Consolidate expired-ban cleanup into a single block that always returns
next(), use raw keys (req.ip, userId) for banLogs.delete(), add an
expiresAt validity guard, await cache writes with error logging, and
parallelize independent I/O with Promise.all.
Add 25 tests covering all checkBan code paths including the specific
regressions for IP-only cleanup, Redis key mismatch, missing expiresAt,
and cache write failures.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: automatic logout_hint fallback for long OpenID tokens
Implements OIDC RP-Initiated Logout cascading strategy to prevent errors when id_token_hint makes logout URL too long.
Automatically detects URLs exceeding configurable length and falls back to logout_hint only when URL is too long, preserving previous behavior when token is missing. Adds OPENID_MAX_LOGOUT_URL_LENGTH environment variable. Comprehensive test coverage with 20 tests. Works with any OpenID provider.
* fix: address review findings for OIDC logout URL length fallback
- Replace two-boolean tri-state (useIdTokenHint/urlTooLong) with a single
string discriminant ('use_token'|'too_long'|'no_token') for clarity
- Fix misleading warning: differentiate 'url too long + no client_id' from
'no token + no client_id' so operators get actionable advice
- Strict env var parsing: reject partial numeric strings like '500abc' that
Number.parseInt silently accepted; use regex + Number() instead
- Pre-compute projected URL length from base URL + token length (JWT chars
are URL-safe), eliminating the set-then-delete mutation pattern
- Extract parseMaxLogoutUrlLength helper for validation and early return
- Add tests: invalid env values, url-too-long + missing OPENID_CLIENT_ID,
boundary condition (exact max vs max+1), cookie-sourced long token
- Remove redundant try/finally in 'respects custom limit' test
- Use empty value in .env.example to signal optional config (default: 2000)
---------
Co-authored-by: Airam Hernández Hernández <airam.hernandez@intelequia.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: avoid express-rate-limit v8 ERR_ERL_KEY_GEN_IPV6 false positive
express-rate-limit v8 calls keyGenerator.toString() and throws
ERR_ERL_KEY_GEN_IPV6 if the source contains the literal substring
"req.ip" without "ipKeyGenerator". When packages/api compiles
req?.ip to older JS targets, the output contains "req.ip",
triggering the heuristic.
Bracket notation (req?.['ip']) produces identical runtime behavior
but never emits the literal "req.ip" substring regardless of
compilation target.
Closes#12321
* fix: add toString regression test and clean up redundant annotation
Add a test that verifies removePorts.toString() does not contain
"req.ip", guarding against reintroduction of the ERR_ERL_KEY_GEN_IPV6
false positive. Fix a misleading test description and remove a
redundant type annotation on a trivially-inferred local.
* security: replace JSZip metadata guard with yauzl streaming decompression
The ODT decompressed-size guard was checking JSZip's private
_data.uncompressedSize fields, which are populated from the ZIP central
directory — attacker-controlled metadata. A crafted ODT with falsified
uncompressedSize values bypassed the 50MB cap entirely, allowing
content.xml decompression to exhaust Node.js heap memory (DoS).
Replace JSZip with yauzl for ODT extraction. The new extractOdtContentXml
function uses yauzl's streaming API: it lazily iterates ZIP entries,
opens a decompression stream for content.xml, and counts real bytes as
they arrive from the inflate stream. The stream is destroyed the moment
the byte count crosses ODT_MAX_DECOMPRESSED_SIZE, aborting the inflate
before the full payload is materialised in memory.
- Remove jszip from direct dependencies (still transitive via mammoth)
- Add yauzl + @types/yauzl
- Update zip-bomb test to verify streaming abort with DEFLATE payload
* fix: close file descriptor leaks and declare jszip test dependency
- Use a shared `finish()` helper in extractOdtContentXml that calls
zipfile.close() on every exit path (success, size cap, missing entry,
openReadStream errors, zipfile errors). Without this, any error path
leaked one OS file descriptor permanently — uploading many malformed
ODTs could exhaust the process FD limit (a distinct DoS vector).
- Add jszip to devDependencies so the zip-bomb test has an explicit
dependency rather than relying on mammoth's transitive jszip.
- Update JSDoc to document that all exit paths close the zipfile.
* fix: move yauzl from dependencies to peerDependencies
Matches the established pattern for runtime parser libraries in
packages/api: mammoth, pdfjs-dist, and xlsx are all peerDependencies
(provided by the consuming /api workspace) with devDependencies for
testing. yauzl was incorrectly placed in dependencies.
* fix: add yauzl to /api dependencies to satisfy peer dep
packages/api declares yauzl as a peerDependency; /api is the consuming
workspace that must provide it at runtime, matching the pattern used
for mammoth, pdfjs-dist, and xlsx.
* fix: Add removePorts keyGenerator to all IP-based rate limiters
Six IP-based rate limiters are missing the `keyGenerator: removePorts`
option that is already used by the auth-related limiters (login,
register, resetPassword, verifyEmail). Without it, reverse proxies that
include ports in X-Forwarded-For headers cause
ERR_ERL_INVALID_IP_ADDRESS errors from express-rate-limit.
Fixes#12318
* fix: make removePorts IPv6-safe to prevent rate-limit key collisions
The original regex `/:\d+[^:]*$/` treated the last colon-delimited
segment of bare IPv6 addresses as a port, mangling valid IPs
(e.g. `::1` → `::`, `2001:db8::1` → `2001:db8::`). Distinct IPv6
clients could collapse into the same rate-limit bucket.
Use `net.isIP()` as a fast path for already-valid IPs, then match
bracketed IPv6+port and IPv4+port explicitly. Bare IPv6 addresses
are now returned unchanged.
Also fixes pre-existing property ordering inconsistency in
ttsLimiters.js userLimiterOptions (keyGenerator before store).
* refactor: move removePorts to packages/api as TypeScript, fix import order
- Move removePorts implementation to packages/api/src/utils/removePorts.ts
with proper Express Request typing
- Reduce api/server/utils/removePorts.js to a thin re-export from
@librechat/api for backward compatibility
- Consolidate removePorts import with limiterCache from @librechat/api
in all 6 limiter files, fixing import order (package imports shortest
to longest, local imports longest to shortest)
- Remove narrating inline comments per code style guidelines
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
- Bump fast-xml-parser dependency from 5.5.6 to 5.5.7 for improved functionality and compatibility.
- Update corresponding entries in both package.json and package-lock.json to reflect the new version.
* fix: use ACL ownership for prompt group cleanup on user deletion
deleteUserPrompts previously called getAllPromptGroups with only an
author filter, which defaults to searchShared=true and drops the
author filter for shared/global project entries. This caused any user
deleting their account to strip shared prompt group associations and
ACL entries for other users.
Replace the author-based query with ACL-based ownership lookup:
- Find prompt groups where the user has OWNER permission (DELETE bit)
- Only delete groups where the user is the sole owner
- Preserve multi-owned groups and their ACL entries for other owners
* fix: use ACL ownership for agent cleanup on user deletion
deleteUserAgents used the deprecated author field to find and delete
agents, then unconditionally removed all ACL entries for those agents.
This could destroy ACL entries for agents shared with or co-owned by
other users.
Replace the author-based query with ACL-based ownership lookup:
- Find agents where the user has OWNER permission (DELETE bit)
- Only delete agents where the user is the sole owner
- Preserve multi-owned agents and their ACL entries for other owners
- Also clean up handoff edges referencing deleted agents
* fix: add MCP server cleanup on user deletion
User deletion had no cleanup for MCP servers, leaving solely-owned
servers orphaned in the database with dangling ACL entries for other
users.
Add deleteUserMcpServers that follows the same ACL ownership pattern
as prompt groups and agents: find servers with OWNER permission,
check for sole ownership, and only delete those with no other owners.
* style: fix prettier formatting in Prompt.spec.js
* refactor: extract getSoleOwnedResourceIds to PermissionService
The ACL sole-ownership detection algorithm was duplicated across
deleteUserPrompts, deleteUserAgents, and deleteUserMcpServers.
Centralizes the three-step pattern (find owned entries, find other
owners, compute sole-owned set) into a single reusable utility.
* refactor: use getSoleOwnedResourceIds in all deletion functions
- Replace inline ACL queries with the centralized utility
- Remove vestigial _req parameter from deleteUserPrompts
- Use Promise.all for parallel project removal instead of sequential awaits
- Disconnect live MCP sessions and invalidate tool cache before deleting
sole-owned MCP server documents
- Export deleteUserMcpServers for testability
* test: improve deletion test coverage and quality
- Move deleteUserPrompts call to beforeAll to eliminate execution-order
dependency between tests
- Standardize on test() instead of it() for consistency in Prompt.spec.js
- Add assertion for deleting user's own ACL entry preservation on
multi-owned agents
- Add deleteUserMcpServers integration test suite with 6 tests covering
sole-owner deletion, multi-owner preservation, session disconnect,
cache invalidation, model-not-registered guard, and missing MCPManager
- Add PermissionService mock to existing deleteUser.spec.js to fix
import chain
* fix: add legacy author-based fallback for unmigrated resources
Resources created before the ACL system have author set but no AclEntry
records. The sole-ownership detection returns empty for these, causing
deleteUserPrompts, deleteUserAgents, and deleteUserMcpServers to silently
skip them — permanently orphaning data on user deletion.
Add a fallback that identifies author-owned resources with zero ACL
entries (truly unmigrated) and includes them in the deletion set. This
preserves the multi-owner safety of the ACL path while ensuring pre-ACL
resources are still cleaned up regardless of migration status.
* style: fix prettier formatting across all changed files
* test: add resource type coverage guard for user deletion
Ensures every ResourceType in the ACL system has a corresponding cleanup
handler wired into deleteUserController. When a new ResourceType is added
(e.g. WORKFLOW), this test fails immediately, preventing silent data
orphaning on user account deletion.
* style: fix import order in PermissionService destructure
* test: add opt-out set and fix test lifecycle in coverage guard
Add NO_USER_CLEANUP_NEEDED set for resource types that legitimately
require no per-user deletion. Move fs.readFileSync into beforeAll so
path errors surface as clean test failures instead of unhandled crashes.
Cap adjustTimestampsForOrdering to N passes and add cycle detection
to findValidParent, preventing DoS via crafted ChatGPT export files
with cyclic parentMessageId relationships.
Add breakParentCycles to sever cyclic back-edges before saving,
ensuring structurally valid message trees are persisted to the DB.
- Added `apk upgrade --no-cache` to both Dockerfile and Dockerfile.multi to ensure all installed packages are up to date.
- Maintained the installation of `jemalloc` and other dependencies without changes.
- Bump @dicebear/collection and @dicebear/core to version 9.4.1 across multiple package files for consistency and improved functionality.
- Update related dependencies in the client and packages/client directories to ensure compatibility with the new versions.
* 🔐 fix: Reject OpenID email fallback when stored openidId mismatches token sub
When `findOpenIDUser` falls back to email lookup after the primary
`openidId`/`idOnTheSource` query fails, it now rejects any user whose
stored `openidId` differs from the incoming JWT subject claim. This
closes an account-takeover vector where a valid IdP JWT containing a
victim's email but a different `sub` could authenticate as the victim
when OPENID_REUSE_TOKENS is enabled.
The migration path (user has no `openidId` yet) is unaffected.
* test: Validate openidId mismatch guard in email fallback path
Update `findOpenIDUser` unit tests to assert that email-based lookups
returning a user with a different `openidId` are rejected with
AUTH_FAILED. Add matching integration test in `openIdJwtStrategy.spec`
exercising the full verify callback with the real `findOpenIDUser`.
* 🔐 fix: Remove redundant `openidId` truthiness check from mismatch guard
The `&& openidId` middle term in the guard condition caused it to be
bypassed when the incoming token `sub` was empty or undefined. Since
the JS callers can pass `payload?.sub` (which may be undefined), this
created a path where the guard never fired and the email fallback
returned the victim's account. Removing the term ensures the guard
rejects whenever the stored openidId differs from the incoming value,
regardless of whether the incoming value is falsy.
* test: Cover falsy openidId bypass and openidStrategy mismatch rejection
Add regression test for the guard bypass when `openidId` is an empty
string and the email lookup finds a user with a stored openidId.
Add integration test in openidStrategy.spec.js exercising the
mismatch rejection through the full processOpenIDAuth callback,
ensuring both OIDC paths (JWT reuse and standard callback) are
covered.
Restore intent-documenting comment on the no-provider fixture.
* 🔧 fix: Isolate HTTP agents for code-server axios requests
Prevents socket hang up after 5s on Node 19+ when code executor has
file attachments. follow-redirects (axios dep) leaks `socket.destroy`
as a timeout listener on TCP sockets; with Node 19+ defaulting to
keepAlive: true, tainted sockets re-enter the global pool and destroy
active node-fetch requests in CodeExecutor after the idle timeout.
Uses dedicated http/https agents with keepAlive: false for all axios
calls targeting CODE_BASEURL in crud.js and process.js.
Closes#12298
* ♻️ refactor: Extract code-server HTTP agents to shared module
- Move duplicated agent construction from crud.js and process.js into
a shared agents.js module to eliminate DRY violation
- Switch process.js from raw `require('axios')` to `createAxiosInstance()`
for proxy configuration parity with crud.js
- Fix import ordering in process.js (agent constants no longer split imports)
- Add 120s timeout to uploadCodeEnvFile (was the only code-server call
without a timeout)
* ✅ test: Add regression tests for code-server socket isolation
- Add crud.spec.js covering getCodeOutputDownloadStream and
uploadCodeEnvFile (agent options, timeout, URL, error handling)
- Add socket pool isolation tests to process.spec.js asserting
keepAlive:false agents are forwarded to axios
- Update process.spec.js mocks for createAxiosInstance() migration
* ♻️ refactor: Move code-server agents to packages/api
Relocate agents.js from api/server/services/Files/Code/ to
packages/api/src/utils/code.ts per workspace conventions. Consumers
now import codeServerHttpAgent/codeServerHttpsAgent from @librechat/api.
* fix: add ODT support to native document parser
* fix: replace execSync with jszip for ODT parsing
* docs: update documentParserMimeTypes comment to include odt
* fix: improve ODT XML extraction and add empty.odt fixture
- Scope extraction to <office:body> to exclude metadata/style nodes
- Map </text:p> and </text:h> closings to newlines, preserving paragraph
structure instead of collapsing everything to a single line
- Handle <text:line-break/> as explicit newlines
- Strip remaining tags, normalize horizontal whitespace, cap consecutive
blank lines at one
- Regenerate sample.odt as a two-paragraph fixture so the test exercises
multi-paragraph output
- Add empty.odt fixture and test asserting 'No text found in document'
* fix: address review findings in ODT parser
- Use static `import JSZip from 'jszip'` instead of dynamic import;
jszip is CommonJS-only with no ESM/Jest-isolation concern (F1)
- Decode the five standard XML entities after tag-stripping so
documents with &, <, >, ", ' send correct text to the LLM (F2)
- Remove @types/jszip devDependency; jszip ships bundled declarations
and @types/jszip is a stale 2020 stub that would shadow them (F3)
- Handle <text:tab/> → \t and <text:s .../> → ' ' before the generic
tag stripper so tab-aligned and multi-space content is preserved (F4)
- Add sample-entities.odt fixture and test covering entity decoding,
tab, and spacing-element handling (F5)
- Rename 'throws for empty odt' → 'throws for odt with no extractable
text' to distinguish from a zero-byte/corrupt file case (F8)
* fix: add decompressed content size cap to odtToText (F6)
Reads uncompressed entry sizes from the JSZip internal metadata before
extracting any content. Throws if the total exceeds 50MB, preventing a
crafted ODT with a high-ratio compressed payload from exhausting heap.
Adds a corresponding test using a real DEFLATE-compressed ZIP (~51KB on
disk, 51MB uncompressed) to verify the guard fires before any extraction.
* fix: add java to codeTypeMapping for file upload support
.java files were rejected with "Unable to determine file type" because
browsers send an empty MIME type for them and codeTypeMapping had no
'java' entry for inferMimeType() to fall back on.
text/x-java was already present in all five validation lists
(fullMimeTypesList, codeInterpreterMimeTypesList, retrievalMimeTypesList,
textMimeTypes, retrievalMimeTypes), so mapping to it (not text/plain)
ensures .java uploads work for both File Search and Code Interpreter.
Closes#12307
* fix: address follow-up review findings (A-E)
A: regenerate package-lock.json after removing @types/jszip from
package.json; without this npm ci was still installing the stale
2020 type stubs and TypeScript was resolving against them
B: replace dynamic import('jszip') in the zip-bomb test with the same
static import already used in production; jszip is CJS-only with no
ESM/Jest isolation concern
C: document that the _data.uncompressedSize guard fails open if jszip
renames the private field (accepted limitation, test would catch it)
D: rename 'preserves tabs' test to 'normalizes tab and spacing elements
to spaces' since <text:tab> is collapsed to a space, not kept as \t
E: fix test.each([ formatting artifact (missing newline after '[')
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: Set response format for agent tools in DALLE3, FluxAPI, and StableDiffusion classes
- Added logic to set `responseFormat` to 'content_and_artifact' when `isAgent` is true in DALLE3.js, FluxAPI.js, and StableDiffusion.js.
* test: Add regression tests for image tool agent mode in imageTools-agent.spec.js
- Introduced a new test suite for DALLE3, FluxAPI, and StableDiffusion classes to verify that the invoke() method returns a ToolMessage with base64 in artifact.content, ensuring it is not serialized into content.
- Validated that responseFormat is set to 'content_and_artifact' when isAgent is true, and confirmed the correct handling of base64 data in the response.
* fix: handle agent error paths and generateFinetunedImage in image tools
- StableDiffusion._call() was returning a raw string on API error, bypassing
returnValue() and breaking the content_and_artifact contract when isAgent is true
- FluxAPI.generateFinetunedImage() had no isAgent branch; it would call
processFileURL (unset in agent context) instead of fetching and returning
the base64 image as an artifact tuple
- Add JSDoc to all three responseFormat assignments clarifying why LangChain
requires this property for correct ToolMessage construction
* test: expand image tool agent mode regression suite
- Add env var save/restore in beforeEach/afterEach to prevent test pollution
- Add error path tests for all three tools verifying ToolMessage content and
artifact are correctly populated when the upstream API fails
- Add generate_finetuned action test for FluxAPI covering the new agent branch
in generateFinetunedImage
* chore: fix lint errors in FluxAPI and imageTools-agent spec
* chore: fix import ordering in imageTools-agent spec
* 🧯 fix: Remove revoked agents from user favorites
When agent access is revoked, the agent remained in the user's favorites
causing repeated 403 errors on page load. Backend now cleans up favorites
on permission revocation; frontend treats 403 like 404 and auto-removes
stale agent references.
* 🧪 fix: Address review findings for stale agent favorites cleanup
- Guard cleanup effect with ref to prevent infinite loop on mutation
failure (Finding 1)
- Use validated results.revoked instead of raw request payload for
revokedUserIds (Finding 3)
- Stabilize staleAgentIds memo with string key to avoid spurious
re-evaluation during drag-drop (Finding 5)
- Add JSDoc with param types to removeRevokedAgentFromFavorites
(Finding 7)
- Return promise from removeRevokedAgentFromFavorites for testability
- Add 7 backend tests covering revocation cleanup paths
- Add 3 frontend tests for 403 handling and stale cleanup persistence
* fix: set explicit permission defaults for USER role in roleDefaults
Previously several permission types for the USER role had empty
objects in roleDefaults, causing the getPermissionValue fallback to
resolve SHARE/CREATE via the zod schema defaults on fresh installs.
This silently granted users MCP server creation ability and left
share permissions ambiguous.
Sets explicit defaults for all multi-field permission types:
- PROMPTS/AGENTS: USE and CREATE true, SHARE false
- MCP_SERVERS: USE true, CREATE/SHARE false
- REMOTE_AGENTS: all false
Adds regression tests covering the exact reported scenarios (fresh
install with `agents: { use: true }`, restart preserving admin-panel
overrides) and structural guards against future permission schema
expansions missing explicit USER defaults.
Closes#12306.
* fix: guard MCP_SERVERS.CREATE against configDefaults fallback + add migration
The roleDefaults fix alone was insufficient: loadDefaultInterface propagates
configDefaults.mcpServers.create=true as tier-1 in getPermissionValue, overriding
the roleDefault of false. This commit:
- Adds conditional guards for MCP_SERVERS.CREATE and REMOTE_AGENTS.CREATE matching
the existing AGENTS/PROMPTS pattern (only include CREATE when explicitly configured
in yaml OR on fresh install)
- Uses raw interfaceConfig for MCP_SERVERS.CREATE tier-1 instead of loadedInterface
(which includes configDefaults fallback)
- Adds one-time migration backfill: corrects existing MCP_SERVERS.CREATE=true for
USER role in DB when no explicit yaml config is present
- Adds restart-scenario and migration regression tests for MCP_SERVERS
- Cleans up roles.spec.ts: for..of loops, Permissions[] typing, Set for lookups,
removes unnecessary aliases, improves JSDoc for exclusion list
- Fixes misleading test name for agents regression test
- Removes redundant not.toHaveProperty assertions after strict toEqual
* fix: use raw interfaceConfig for REMOTE_AGENTS.CREATE tier-1 (consistency)
Aligns REMOTE_AGENTS.CREATE with the MCP_SERVERS.CREATE fix — reads from
raw interfaceConfig instead of loadedInterface to prevent a future
configDefaults fallback from silently overriding the roleDefault.
* refactor: error handling in useResumableSSE for 404 responses
- Added logic to clear drafts from localStorage when a 404 error occurs.
- Integrated errorHandler to notify users of the error condition.
- Introduced comprehensive tests to validate the new behavior, ensuring drafts are cleared and error handling is triggered correctly.C
* feat: add STREAM_EXPIRED error handling and message localization
- Introduced handling for STREAM_EXPIRED errors in useResumableSSE, updating errorHandler to provide relevant feedback.
- Added a new error message for STREAM_EXPIRED in translation files for user notifications.
- Updated tests to ensure proper error handling and message verification for STREAM_EXPIRED scenarios.
* refactor: replace clearDraft with clearAllDrafts utility
- Removed the clearDraft function from useResumableSSE and useSSE hooks, replacing it with the new clearAllDrafts utility for better draft management.
- Updated localStorage interactions to ensure both text and file drafts are cleared consistently for a conversation.
- Enhanced code readability and maintainability by centralizing draft clearing logic.
* 🔧 chore: Update dependencies in package-lock.json and package.json
- Bump @aws-sdk/client-bedrock-runtime from 3.980.0 to 3.1011.0 and update related dependencies.
- Update fast-xml-parser version from 5.3.8 to 5.5.6 in package.json.
- Adjust various @aws-sdk and @smithy packages to their latest versions for improved functionality and security.
* 🔧 chore: Update @librechat/agents dependency to version 3.1.57 in package.json and package-lock.json
- Bump @librechat/agents from 3.1.56 to 3.1.57 across multiple package files for consistency.
- Remove axios dependency from package.json as it is no longer needed.
* fix: replace manual focus hack with modal menu in MCP selector
Use Ariakit's `modal={true}` instead of a manual `requestAnimationFrame`
focus-restore wrapper, which eliminates the `useRef`/`useCallback`
overhead and lets Ariakit manage focus trapping natively. Also removes
the unused `focusLoop` option from both MCP menu stores and a narrating
comment in MCPSubMenu.
* test: add MCPSelect menu interaction tests
Cover button rendering, menu open/close via click and Escape, and
server toggle keeping the menu open. Renders real MCPServerMenuItem
and StackedMCPIcons components instead of re-implementing their
logic in mocks.
* fix: add unmountOnHide to MCP menu for consistency
Matches the pattern used by MCPSubMenu, BookmarkMenu, and other
Ariakit menus in the codebase. Ensures the menu fully detaches
from the DOM and accessibility tree when closed.
* fix: restore focusLoop on MCP menu stores
Ariakit's CompositeStore (which MenuStore extends) defaults focusLoop
to false. The previous commit incorrectly removed the explicit
focusLoop: true, which silently disabled Arrow-key wraparound
(mandatory per WAI-ARIA Menu pattern). modal={true} only traps Tab
focus — it does not enable Arrow-key looping.
* test: improve MCPSelect test coverage and mock hygiene
- Add aria-modal regression guard so removing modal={true} fails a test
- Add guard branch tests: no MCP access, empty servers, unpinned+empty
- Fix TooltipAnchor mock to correctly spread array children
- Fix import ordering per project conventions
- Move component import to top with other imports
- Replace unasserted jest.fn() mocks with plain values
- Use mutable module-scoped vars for per-test mock overrides
* fix: enhance pointer event handling in FavoriteItem component
Updated the opacity and pointer events logic in the FavoriteItem component to improve user interaction. The changes ensure that the component correctly manages pointer events based on the popover state, enhancing accessibility and usability.
* test: add MCPSubMenu menu interaction tests
Cover guard branch (empty servers), submenu open/close with real
Ariakit components and real MCPServerMenuItem, toggle persistence,
pin/unpin button behavior and aria-label states. Only context
providers and cross-package UI are mocked.
* test: add focusLoop regression guard for both MCP menus
ArrowDown from the last item must wrap to the first — this fails
without focusLoop: true on the menu store, directly guarding the
keyboard accessibility regression that was silently introduced.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🛂 fix: Validate `types` query param in people picker access middleware
checkPeoplePickerAccess only inspected `req.query.type` (singular),
allowing callers to bypass type-specific permission checks by using
the `types` (plural) parameter accepted by the controller. Now both
`type` and `types` are collected and each requested principal type is
validated against the caller's role permissions.
* 🛂 refactor: Hoist valid types constant, improve logging, and add edge-case tests
- Hoist VALID_PRINCIPAL_TYPES to module-level Set to avoid per-request allocation
- Include both `type` and `types` in error log for debuggability
- Restore detailed JSDoc documenting per-type permission requirements
- Add missing .json() assertion on partial-denial test
- Add edge-case tests: all-invalid types, empty string types, PrincipalType.PUBLIC
* 🏷️ fix: Align TPrincipalSearchParams with actual controller API
The stale type used `type` (singular) but the controller and all callers
use `types` (plural array). Aligns with PrincipalSearchParams in
types/queries.ts.
Prevent memory exhaustion DoS by rejecting documents exceeding 15MB
before reading them into memory, closing the gap between the 512MB
upload limit and unbounded in-memory parsing.
- Extract `specDisplayFieldReset` constant and `mergeQuerySettingsWithSpec`
utility to `client/src/utils/endpoints.ts` as a single source of truth
for spec display fields that must be cleared on non-spec transitions.
- Clear `spec`, `iconURL`, `modelLabel`, and `greeting` from the merged
preset in `ChatRoute.getNewConvoPreset()` when URL query parameters
override the conversation without explicitly setting a spec.
- Also clear `greeting` in the parallel cleanup in `useQueryParams.newQueryConvo`
using the shared `specDisplayFieldReset` constant.
- Guard the field reset on `specPreset != null` so null values aren't
injected when no spec is configured.
- Add comprehensive test coverage for the merge-and-clear logic.
* fix: strip protocol from domain before encoding in `domainParser`
All https:// (and http://) domains produced the same 10-char base64
prefix due to ENCODED_DOMAIN_LENGTH truncation, causing tool name
collisions for agents with multiple actions.
Strip the protocol before encoding so the base64 key is derived from
the hostname. Add `legacyDomainEncode` to preserve the old encoding
logic for backward-compatible matching of existing stored actions.
* fix: backward-compatible tool matching in ToolService
Update `getActionToolDefinitions` to match stored tools against both
new and legacy domain encodings. Update `loadActionToolsForExecution`
to resolve model-called tool names via a `normalizedToDomain` map
that includes both encoding variants, with legacy fallback for
request builder lookup.
* fix: action route save/delete domain encoding issues
Save routes now remove old tools matching either new or legacy domain
encoding, preventing stale entries when an action's encoding changes
on update.
Delete routes no longer re-encode the already-encoded domain extracted
from the stored actions array, which was producing incorrect keys and
leaving orphaned tools.
* test: comprehensive coverage for action domain encoding
Rewrite ActionService tests to cover real matching patterns used by
ToolService and action routes. Tests verify encode/decode round-trips,
protocol stripping, backward-compatible tool name matching at both
definition and execution phases, save-route cleanup of old/new
encodings, delete-route domain extraction, and the collision fix for
multi-action agents.
* fix: add legacy domain compat to all execution paths, make legacyDomainEncode sync
CRITICAL: processRequiredActions (assistants path) was not updated with
legacy domain matching — existing assistants with https:// domain actions
would silently fail post-deployment because domainMap only had new encoding.
MAJOR: loadAgentTools definitionsOnly=false path had the same issue.
Both now use a normalizedToDomain map with legacy+new entries and extract
function names via the matched key (not the canonical domain).
Also: make legacyDomainEncode synchronous (no async operations), store
legacyNormalized in processedActionSets to eliminate recomputation in
the per-tool fallback, and hoist domainSeparatorRegex to module level.
* refactor: clarify domain variable naming and tool-filter helpers in action routes
Rename shadowed 'domain' to 'encodedDomain' to separate raw URL from
encoded key in both agent and assistant save routes.
Rename shouldRemoveTool to shouldRemoveAgentTool / shouldRemoveAssistantTool
to make the distinct data-shape guards explicit.
Remove await on now-synchronous legacyDomainEncode.
* test: expand coverage for all review findings
- Add validateAndUpdateTool tests (protocol-stripping match logic)
- Restore unicode domain encode/decode/round-trip tests
- Add processRequiredActions matching pattern tests (assistants path)
- Add legacy guard skip test for short bare hostnames
- Add pre-normalized Set test for definition-phase optimization
- Fix corrupt-cache test to assert typeof instead of toBeDefined
- Verify legacyDomainEncode is synchronous (not a Promise)
- Remove all await on legacyDomainEncode (now sync)
58 tests, up from 44.
* fix: address follow-up review findings A-E
A: Fix stale JSDoc @returns {Promise<string>} on now-synchronous
legacyDomainEncode — changed to @returns {string}.
B: Rename normalizedToDomain to domainLookupMap in processRequiredActions
and loadAgentTools where keys are raw encoded domains (not normalized),
avoiding confusion with loadActionToolsForExecution where keys ARE
normalized.
C: Pre-normalize actionToolNames into a Set<string> in
getActionToolDefinitions, replacing O(signatures × tools) per-check
.some() + .replace() with O(1) Set.has() lookups.
D: Remove stripProtocol from ActionService exports — it is a one-line
internal helper. Spec tests for it removed; behavior is fully covered
by domainParser protocol-stripping tests.
E: Fix pre-existing bug where processRequiredActions re-loaded action
sets on every missing-tool iteration. The guard !actionSets.length
always re-triggered because actionSets was reassigned to a plain
object (whose .length is undefined). Replaced with a null-check
on a dedicated actionSetsData variable.
* fix: strip path and query from domain URLs in stripProtocol
URLs like 'https://api.example.com/v1/endpoint?foo=bar' previously
retained the path after protocol stripping, contaminating the encoded
domain key. Now strips everything after the first '/' following the
host, using string indexing instead of URL parsing to avoid punycode
normalization of unicode hostnames.
Closes Copilot review comments 1, 2, and 5.
- Strip deploy: true and deploy-deployment/namespace from docker-publish.yml
- Delete hanzo-deploy.yml.disabled (was already disabled)
- Images still build and push to GHCR as before
* 🔒 fix: Prevent token leaks to MCP server on OAuth discovery failure
When OAuth metadata discovery fails, refresh logic was falling back to
POSTing refresh tokens to /token on the MCP resource server URL instead
of the authorization server. A malicious MCP server could exploit this
by blocking .well-known discovery to harvest refresh tokens.
Changes:
- Replace unsafe /token fallback with hard error in both refresh paths
- Thread stored token_endpoint (SSRF-validated during initial flow)
through the refresh chain so legacy servers without .well-known still
work after the first successful auth
- Fix revokeOAuthToken to always SSRF-validate the revocation URL,
including the /revoke fallback path
- Redact refresh token and credentials from debug-level log output
- Split branch 2 compound condition for consistent error messages
* ✅ test: Add stored endpoint fallback tests and improve refresh coverage
- Add storedTokenEndpoint fallback tests for both refresh branches
- Add missing test for branch 2 metadata-without-token_endpoint case
- Rename misleading test name to match actual mock behavior
- Split auto-discovered throw test into undefined vs missing-endpoint
- Remove redundant afterEach mockFetch.mockClear() calls (already
covered by jest.clearAllMocks() in beforeEach)
* 🔒 fix: Remove OpenID federated tokens from refresh endpoint response
The refresh controller was attaching federatedTokens (including the
refresh_token) to the user object returned in the JSON response,
exposing HttpOnly-protected tokens to client-side JavaScript.
The tokens are already stored server-side by setOpenIDAuthTokens
and re-attached by the JWT strategy on authenticated requests.
* 🔒 fix: Strip sensitive fields from OpenID refresh response user object
The OpenID refresh path returned the raw findOpenIDUser result without
field projection, unlike the non-OpenID path which excludes password,
__v, totpSecret, and backupCodes via getUserById projection. Destructure
out sensitive fields before serializing.
Also strengthens the regression test: uses not.toHaveProperty for true
property-absence checks (expect.anything() misses null/undefined), adds
positive shape assertion, and DRYs up duplicated mock user setup.
* 🔒 fix: Validate conversation ownership in remote agent API endpoints
Add user-scoped ownership checks for client-supplied conversation IDs
in OpenAI-compatible and Open Responses controllers to prevent
cross-tenant file/message loading via IDOR.
* 🔒 fix: Harden ownership checks against type confusion and unhandled errors
- Add typeof string validation before getConvo to block NoSQL operator
injection (e.g. { "$gt": "" }) bypassing the ownership check
- Move ownership checks inside try/catch so DB errors produce structured
JSON error responses instead of unhandled promise rejections
- Add string type validation for conversation_id and previous_response_id
in the upstream TS request validators (defense-in-depth)
* 🧪 test: Add coverage for conversation ownership validation in remote agent APIs
- Fix broken getConvo mock in openai.spec.js (was missing entirely)
- Add tests for: owned conversation, unowned (404), non-string type (400),
absent conversation_id (skipped), and DB error (500) — both controllers
* 🔒 fix: Resolve env vars before body placeholder expansion to prevent secret exfiltration
Body placeholders ({{LIBRECHAT_BODY_*}}) were substituted before
extractEnvVariable ran, allowing user-controlled body fields containing
${SECRET} patterns to be expanded into real environment values in
outbound headers. Reorder so env vars resolve first, preventing
untrusted input from triggering env expansion.
* 🛡️ fix: Block sensitive infrastructure env vars from placeholder resolution
Add isSensitiveEnvVar blocklist to extractEnvVariable so that internal
infrastructure secrets (JWT_SECRET, JWT_REFRESH_SECRET, CREDS_KEY,
CREDS_IV, MEILI_MASTER_KEY, MONGO_URI, REDIS_URI, REDIS_PASSWORD)
can never be resolved via ${VAR} expansion — even if an attacker
manages to inject a placeholder pattern.
Uses exact-match set (not substring patterns) to avoid breaking
legitimate operator config that references OAuth/API secrets in
MCP and custom endpoint configurations.
* 🧹 test: Rename ANOTHER_SECRET test fixture to ANOTHER_VALUE
Avoid using SECRET-containing names for non-sensitive test fixtures
to prevent confusion with the new isSensitiveEnvVar blocklist.
* 🔒 fix: Resolve env vars before all user-controlled substitutions in processSingleValue
Move extractEnvVariable to run on the raw admin-authored template
BEFORE customUserVars, user fields, OIDC tokens, and body placeholders.
Previously env resolution ran after customUserVars, so a user setting
a custom MCP variable to "${SECRET}" could still trigger env expansion.
Now env vars are resolved strictly on operator config, and all
subsequent user-controlled substitutions cannot introduce ${VAR} patterns
that would be expanded.
Gated by !dbSourced so DB-stored servers continue to skip env resolution.
Adds a security-invariant comment documenting the ordering requirement.
* 🧪 test: Comprehensive security regression tests for placeholder injection
- Cover all three body fields (conversationId, parentMessageId, messageId)
- Add user-field injection test (user.name containing ${VAR})
- Add customUserVars injection test (MY_TOKEN = "${VAR}")
- Add processMCPEnv injection tests for body and customUserVars paths
- Remove redundant process.env setup/teardown already handled by beforeEach/afterEach
* 🧹 chore: Add REDIS_PASSWORD to blocklist integration test; document customUserVars gate
* fix: exempt allowedDomains from MCP OAuth SSRF checks (#12254)
The SSRF guard in validateOAuthUrl was context-blind — it blocked
private/internal OAuth endpoints even for admin-trusted MCP servers
listed in mcpSettings.allowedDomains. Add isHostnameAllowed() to
domain.ts and skip SSRF checks in validateOAuthUrl when the OAuth
endpoint hostname matches an allowed domain.
* refactor: thread allowedDomains through MCP connection stack
Pass allowedDomains from MCPServersRegistry through BasicConnectionOptions,
MCPConnectionFactory, and into MCPOAuthHandler method calls so the OAuth
layer can exempt admin-trusted domains from SSRF validation.
* test: add allowedDomains bypass tests and fix registry mocks
Add isHostnameAllowed unit tests (exact, wildcard, case-insensitive,
private IPs). Add MCPOAuthSecurity tests covering the allowedDomains
bypass for initiateOAuthFlow, refreshOAuthTokens, and revokeOAuthToken.
Update registry mocks to include getAllowedDomains.
* fix: enforce protocol/port constraints in OAuth allowedDomains bypass
Replace isHostnameAllowed (hostname-only check) with isOAuthUrlAllowed
which parses the full OAuth URL and matches against allowedDomains
entries including protocol and explicit port constraints — mirroring
isDomainAllowedCore's allowlist logic. Prevents a port-scoped entry
like 'https://auth.internal:8443' from also exempting other ports.
* test: cover auto-discovery and branch-3 refresh paths with allowedDomains
Add three new integration tests using a real OAuth test server:
- auto-discovered OAuth endpoints allowed when server IP is in allowedDomains
- auto-discovered endpoints rejected when allowedDomains doesn't match
- refreshOAuthTokens branch 3 (no clientInfo/config) with allowedDomains bypass
Also rename describe block from ephemeral issue number to durable name.
* docs: explain intentional absence of allowedDomains in completeOAuthFlow
Prevents future contributors from assuming a missing parameter during
security audits — URLs are pre-validated during initiateOAuthFlow.
* test: update initiateOAuthFlow assertion for allowedDomains parameter
* perf: avoid redundant URL parse for admin-trusted OAuth endpoints
Move isOAuthUrlAllowed check before the hostname extraction so
admin-trusted URLs short-circuit with a single URL parse instead
of two. The hostname extraction (new URL) is now deferred to the
SSRF-check path where it's actually needed.
* 🔏 fix: Apply agent access control filtering to context/OCR resource loading
The context/OCR file path in primeResources fetched files by file_id
without applying filterFilesByAgentAccess, unlike the file_search and
execute_code paths. Add filterFiles dependency injection to primeResources
and invoke it after getFiles to enforce consistent access control.
* fix: Wire filterFilesByAgentAccess into all agent initialization callers
Pass the filterFilesByAgentAccess function from the JS layer into the TS
initializeAgent → primeResources chain via dependency injection, covering
primary, handoff, added-convo, and memory agent init paths.
* test: Add access control filtering tests for primeResources
Cover filterFiles invocation with context/OCR files, verify filtering
rejects inaccessible files, and confirm graceful fallback when filterFiles,
userId, or agentId are absent.
* fix: Guard filterFilesByAgentAccess against ephemeral agent IDs
Ephemeral agents have no DB document, so getAgent returns null and the
access map defaults to all-false, silently blocking all non-owned files.
Short-circuit with isEphemeralAgentId to preserve the pass-through
behavior for inline-built agents (memory, tool agents).
* fix: Clean up resources.ts and JS caller import order
Remove redundant optional chain on req.user.role inside user-guarded
block, update primeResources JSDoc with filterFiles and agentId params,
and reorder JS imports to longest-to-shortest per project conventions.
* test: Strengthen OCR assertion and add filterFiles error-path test
Use toHaveBeenCalledWith for the OCR filtering test to verify exact
arguments after the OCR→context merge step. Add test for filterFiles
rejection to verify graceful degradation (logs error, returns original
tool_resources).
* fix: Correct import order in addedConvo.js and initialize.js
Sort by total line length descending: loadAddedAgent (91) before
filterFilesByAgentAccess (84), loadAgentTools (91) before
filterFilesByAgentAccess (84).
* test: Add unit tests for filterFilesByAgentAccess and hasAccessToFilesViaAgent
Cover every branch in permissions.js: ephemeral agent guard, missing
userId/agentId/files early returns, all-owned short-circuit, mixed
owned + non-owned with VIEW/no-VIEW, agent-not-found fail-closed,
author path scoped to attached files, EDIT gate on delete, DB error
fail-closed, and agent with no tool_resources.
* test: Cover file.user undefined/null in permissions spec
Files with no user field fall into the non-owned path and get run
through hasAccessToFilesViaAgent. Add two cases: attached file with
no user field is returned, unattached file with no user field is
excluded.
* fix: gate action tools by actions capability in all code paths
Extract resolveAgentCapabilities helper to eliminate 3x-duplicated
capability resolution. Apply early action-tool filtering in both
loadToolDefinitionsWrapper and loadAgentTools non-definitions path.
Gate loadActionToolsForExecution in loadToolsForExecution behind an
actionsEnabled parameter with a cache-based fallback. Replace the
late capability guard in loadAgentTools with a hasActionTools check
to avoid unnecessary loadActionSets DB calls and duplicate warnings.
* fix: thread actionsEnabled through InitializedAgent type
Add actionsEnabled to the loadTools callback return type,
InitializedAgent, and the initializeAgent destructuring/return
so callers can forward the resolved value to loadToolsForExecution
without redundant getEndpointsConfig cache lookups.
* fix: pass actionsEnabled from callers to loadToolsForExecution
Thread actionsEnabled through the agentToolContexts map in
initialize.js (primary and handoff agents) and through
primaryConfig in the openai.js and responses.js controllers,
avoiding per-tool-call capability re-resolution on the hot path.
* test: add regression tests for action capability gating
Test the real exported functions (resolveAgentCapabilities,
loadAgentTools, loadToolsForExecution) with mocked dependencies
instead of shadow re-implementations. Covers definition filtering,
execution gating, actionsEnabled param forwarding, and fallback
capability resolution.
* test: use Constants.EPHEMERAL_AGENT_ID in ephemeral fallback test
Replaces a string guess with the canonical constant to avoid
fragility if the ephemeral detection heuristic changes.
* fix: populate agentToolContexts for addedConvo parallel agents
After processAddedConvo returns, backfill agentToolContexts for
any agents in agentConfigs not already present, so ON_TOOL_EXECUTE
for added-convo agents receives actionsEnabled instead of falling
back to a per-call cache lookup.
* 🛡️ fix: Validate MCP tool authorization on agent create/update
Agent creation and update accepted arbitrary MCP tool strings without
verifying the user has access to the referenced MCP servers. This allowed
a user to embed unauthorized server names in tool identifiers (e.g.
"anything_mcp_<victimServer>"), causing mcpServerNames to be stored on
the agent and granting consumeOnly access via hasAccessViaAgent().
Adds filterAuthorizedTools() that checks MCP tool strings against the
user's accessible server configs (via getAllServerConfigs) before
persisting. Applied to create, update, and duplicate agent paths.
* 🛡️ fix: Harden MCP tool authorization and add test coverage
Addresses review findings on the MCP agent tool authorization fix:
- Wrap getMCPServersRegistry() in try/catch so uninitialized registry
gracefully filters all MCP tools instead of causing a 500 (DoS risk)
- Guard revertAgentVersionHandler: filter unauthorized MCP tools after
reverting to a previous version snapshot
- Preserve existing MCP tools on collaborative updates: only validate
newly added tools, preventing silent stripping of tools the editing
user lacks direct access to
- Add audit logging (logger.warn) when MCP tools are rejected
- Refactor to single-pass lazy-fetch (registry queried only on first
MCP tool encountered)
- Export filterAuthorizedTools for direct unit testing
- Add 18 tests covering: authorized/unauthorized/mixed tools, registry
unavailable fallback, create/update/duplicate/revert handler paths,
collaborative update preservation, and mcpServerNames persistence
* test: Add duplicate handler test, use Constants.mcp_delimiter, DB assertions
- N1: Add duplicateAgentHandler integration test verifying unauthorized
MCP tools are stripped from the cloned agent and mcpServerNames are
correctly persisted in the database
- N2: Replace all hardcoded '_mcp_' delimiter literals with
Constants.mcp_delimiter to prevent silent false-positive tests if
the delimiter value ever changes
- N3: Add DB state assertion to the revert-with-strip test confirming
persisted tools match the response after unauthorized tools are
removed
* fix: Enforce exact 2-segment format for MCP tool keys
Reject MCP tool keys with multiple delimiters to prevent
authorization/execution mismatch when `.pop()` vs `split[1]`
extract different server names from the same key.
* fix: Preserve existing MCP tools when registry is unavailable
When the MCP registry is uninitialized (e.g. server restart), existing
tools already persisted on the agent are preserved instead of silently
stripped. New MCP tools are still rejected when the registry cannot
verify them. Applies to duplicate and revert handlers via existingTools
param; update handler already preserves existing tools via its diff logic.
When Azure AD users belong to 200+ groups, group claims are moved out of
the ID token (overage). The existing resolveGroupsFromOverage() called
Microsoft Graph directly with the app-audience access token, which Graph
rejected (401/403).
Changes:
- Add exchangeTokenForOverage() dedicated OBO exchange with User.Read scope
- Update resolveGroupsFromOverage() to exchange token before Graph call
- Add overage handling to OPENID_ADMIN_ROLE block (was silently failing)
- Share resolved overage groups between required role and admin role checks
- Always resolve via Graph when overage detected (even with partial groups)
- Remove debug-only bypass that forced Graph resolution
- Add tests for OBO exchange, caching, and admin role overage scenarios
Co-authored-by: Airam Hernández Hernández <airam.hernandez@intelequia.com>
* 🛡️ fix: Scope agent-author file access to attached files only
The hasAccessToFilesViaAgent helper short-circuited for agent authors,
granting access to all requested file IDs without verifying they were
attached to the agent's tool_resources. This enabled an IDOR where any
agent author could delete arbitrary files by supplying their agent_id
alongside unrelated file IDs.
Now both the author and non-author paths check file IDs against the
agent's tool_resources before granting access.
* chore: Use Object.values/for...of and add JSDoc in getAttachedFileIds
* test: Add boundary cases for agent file access authorization
- Agent with no tool_resources denies all access (fail-closed)
- Files across multiple resource types are all reachable
- Author + isDelete: true still scopes to attached files only
* 🛡️ fix: Block SSRF via user-provided baseURL in endpoint initialization
User-provided baseURL values (when endpoint is configured with
`user_provided`) were passed through to the OpenAI SDK without
validation. Combined with `directEndpoint`, this allowed arbitrary
server-side requests to internal/metadata URLs.
Adds `validateEndpointURL` that checks against known SSRF targets
and DNS-resolves hostnames to block private IPs. Applied in both
custom and OpenAI endpoint initialization paths.
* 🧪 test: Add validateEndpointURL SSRF tests
Covers unparseable URLs, localhost, private IPs, link-local/metadata,
internal Docker/K8s hostnames, DNS resolution to private IPs, and
legitimate public URLs.
* 🛡️ fix: Add protocol enforcement and import order fix
- Reject non-HTTP/HTTPS schemes (ftp://, file://, data:, etc.) in
validateEndpointURL before SSRF hostname checks
- Document DNS rebinding limitation and fail-open semantics in JSDoc
- Fix import order in custom/initialize.ts per project conventions
* 🧪 test: Expand SSRF validation coverage and add initializer integration tests
Unit tests for validateEndpointURL:
- Non-HTTP/HTTPS schemes (ftp, file, data)
- IPv6 loopback, link-local, and unique-local addresses
- .local and .internal TLD hostnames
- DNS fail-open path (lookup failure allows request)
Integration tests for initializeCustom and initializeOpenAI:
- Guard fires when userProvidesURL is true
- Guard skipped when URL is system-defined or falsy
- SSRF rejection propagates and prevents getOpenAIConfig call
* 🐛 fix: Correct broken env restore in OpenAI initialize spec
process.env was captured by reference, not by value, making the
restore closure a no-op. Snapshot individual env keys before mutation
so they can be properly restored after each test.
* 🛡️ fix: Throw structured ErrorTypes for SSRF base URL validation
Replace plain-string Error throws in validateEndpointURL with
JSON-structured errors using type 'invalid_base_url' (matching new
ErrorTypes.INVALID_BASE_URL enum value). This ensures the client-side
Error component can look up a localized message instead of falling
through to the raw-text default.
Changes across workspaces:
- data-provider: add INVALID_BASE_URL to ErrorTypes enum
- packages/api: throwInvalidBaseURL helper emits structured JSON
- client: add errorMessages entry and localization key
- tests: add structured JSON format assertion
* 🧹 refactor: Use ErrorTypes enum key in Error.tsx for consistency
Replace bare string literal 'invalid_base_url' with computed property
[ErrorTypes.INVALID_BASE_URL] to match every other entry in the
errorMessages map.
* 🛡️ fix: Sanitize markdown artifact rendering to prevent stored XSS
Replace marked-react with react-markdown + remark-gfm for artifact
markdown preview. react-markdown's skipHtml strips raw HTML tags,
and a urlTransform guard blocks javascript: and data: protocol links.
* fix: Update useArtifactProps test to expect react-markdown dependencies
* fix: Harden markdown artifact sanitization
- Convert isSafeUrl from denylist to allowlist (http, https, mailto, tel
plus relative/anchor URLs); unknown protocols are now fail-closed
- Add remark-breaks to restore single-newline-to-<br> behavior that was
silently dropped when replacing marked-react
- Export isSafeUrl from the host module and add 16 direct unit tests
covering allowed protocols, blocked schemes (javascript, data, blob,
vbscript, file, custom), edge cases (empty, whitespace, mixed case)
- Hoist remarkPlugins to a module-level constant to avoid per-render
array allocation in the generated Sandpack component
- Fix import order in generated template (shortest to longest per
AGENTS.md) and remove pre-existing trailing whitespace
* fix: Return null for blocked URLs, add sync-guard comments and test
- urlTransform returns null (not '') for blocked URLs so react-markdown
omits the href/src attribute entirely instead of producing <a href="">
- Hoist urlTransform to module-level constant alongside remarkPlugins
- Add JSDoc sync-guard comments tying the exported isSafeUrl to its
template-string mirror, so future maintainers know to update both
- Add synchronization test asserting the embedded isSafeUrl contains the
same allowlist set, URL parsing, and relative-path checks as the export
* 🛡️ fix: Enforce ACL checks on handoff edge and added-convo agent loading
Edge-linked agents and added-convo agents were fetched by ID via
getAgent without verifying the requesting user's access permissions.
This allowed an authenticated user to reference another user's private
agent in edges or addedConvo and have it initialized at runtime.
Add checkPermission(VIEW) gate in processAgent before initializing
any handoff agent, and in processAddedConvo for non-ephemeral added
agents. Unauthorized agents are logged and added to skippedAgentIds
so orphaned-edge filtering removes them cleanly.
* 🛡️ fix: Validate edge agent access at agent create/update time
Reject agent create/update requests that reference agents in edges
the requesting user cannot VIEW. This provides early feedback and
prevents storing unauthorized agent references as defense-in-depth
alongside the runtime ACL gate in processAgent.
Add collectEdgeAgentIds utility to extract all unique agent IDs from
an edge array, and validateEdgeAgentAccess helper in the v1 handler.
* 🧪 test: Improve ACL gate test coverage and correctness
- Add processAgent ACL gate tests for initializeClient (skip/allow handoff agents)
- Fix addedConvo.spec.js to mock loadAddedAgent directly instead of getAgent
- Seed permMap with ownedAgent VIEW bits in v1.spec.js update-403 test
* 🧹 chore: Remove redundant addedConvo ACL gate (now in middleware)
PR #12243 moved the addedConvo agent ACL check upstream into
canAccessAgentFromBody middleware, making the runtime check in
processAddedConvo and its spec redundant.
* 🧪 test: Rewrite processAgent ACL test with real DB and minimal mocking
Replace heavy mock-based test (12 mocks, Providers.XAI crash) with
MongoMemoryServer-backed integration test that exercises real getAgent,
checkPermission, and AclEntry — only external I/O (initializeAgent,
ToolService, AgentClient) remains mocked. Load edge utilities directly
from packages/api/src/agents/edges to sidestep the config.ts barrel.
* 🧪 fix: Use requireActual spread for @librechat/agents and @librechat/api mocks
The Providers.XAI crash was caused by mocking @librechat/agents with
a minimal replacement object, breaking the @librechat/api initialization
chain. Match the established pattern from client.test.js and
recordCollectedUsage.spec.js: spread jest.requireActual for both
packages, overriding only the functions under test.
* 🛡️ fix: SSRF-validate user-provided URLs in web search auth
User-controlled URL fields (jinaApiUrl, firecrawlApiUrl, searxngInstanceUrl)
flow from plugin auth into outbound HTTP requests without validation.
Reuse existing isSSRFTarget/resolveHostnameSSRF to block private/internal
targets while preserving admin-configured (env var) internal URLs.
* 🛡️ fix: Harden web search SSRF validation
- Reject non-HTTP(S) schemes (file://, ftp://, etc.) in isSSRFUrl
- Conditional write: only assign to authResult after SSRF check passes
- Move isUserProvided tracking after SSRF gate to avoid false positives
- Add authenticated assertions for optional-field SSRF blocks in tests
- Add file:// scheme rejection test
- Wrap process.env mutation in try/finally guard
- Add JSDoc + sync-obligation comment on WEB_SEARCH_URL_KEYS
* 🛡️ fix: Correct auth-type reporting for SSRF-stripped optional URLs
SSRF-stripped optional URL fields no longer pollute isUserProvided.
Track whether the field actually contributed to authResult before
crediting it as user-provided, so categories report SYSTEM_DEFINED
when all surviving values match env vars.
* 🛡️ fix: Enforce MULTI_CONVO and agent ACL checks on addedConvo
addedConvo.agent_id was passed through to loadAddedAgent without any
permission check, enabling an authenticated user to load and execute
another user's private agent via the parallel multi-convo feature.
The middleware now chains a checkAddedConvoAccess gate after the primary
agent check: when req.body.addedConvo is present it verifies the user
has MULTI_CONVO:USE role permission, and when the addedConvo agent_id is
a real (non-ephemeral) agent it runs the same canAccessResource ACL
check used for the primary agent.
* refactor: Harden addedConvo middleware and avoid duplicate agent fetch
- Convert checkAddedConvoAccess to curried factory matching Express
middleware signature: (requiredPermission) => (req, res, next)
- Call checkPermission directly for the addedConvo agent instead of
routing through canAccessResource's tempReq pattern; this avoids
orphaning the resolved agent document and enables caching it on
req.resolvedAddedAgent for downstream loadAddedAgent
- Update loadAddedAgent to use req.resolvedAddedAgent when available,
eliminating a duplicate getAgent DB call per chat request
- Validate addedConvo is a plain object and agent_id is a string
before passing to isEphemeralAgentId (prevents TypeError on object
injection, returns 400-equivalent early exit instead of 500)
- Fix JSDoc: "VIEW access" → "same permission as primary agent",
add @param/@returns to helpers, restore @example on factory
- Fix redundant return await in resolveAgentIdFromBody
* test: Add canAccessAgentFromBody spec covering IDOR fix
26 integration tests using MongoMemoryServer with real models, ACL
entries, and PermissionService — no mocks for core logic.
Covered paths:
- Factory validation (requiredPermission type check)
- Primary agent: missing agent_id, ephemeral, non-agents endpoint
- addedConvo absent / invalid shape (string, array, object injection)
- MULTI_CONVO:USE gate: denied, missing role, ADMIN bypass
- Agent resource ACL: no ACL → 403, insufficient bits → 403,
nonexistent agent → 404, valid ACL → next + cached on req
- End-to-end: both real agents, primary denied short-circuits,
ephemeral primary + real addedConvo
* 🛡️ fix: Fail-closed MCP domain validation for unparseable URLs
`isMCPDomainAllowed` returned true (allow) when `extractMCPServerDomain`
could not parse the URL, treating it identically to a stdio transport.
A URL containing template placeholders or invalid syntax bypassed the
domain allowlist, then `processMCPEnv` resolved it to a valid—and
potentially disallowed—host at connection time.
Distinguish "no URL" (stdio, allowed) from "has URL but unparseable"
(rejected when an allowlist is active) by checking whether `config.url`
is an explicit non-empty string before falling through to the stdio path.
When no allowlist is configured the guard does not fire—unparseable URLs
fall through to connection-level SSRF protection via
`createSSRFSafeUndiciConnect`, preserving legitimate `customUserVars`
template-URL configs.
* test: Expand MCP domain validation coverage for invalid/templated URLs
Cover all branches of the fail-closed guard:
- Invalid/templated URLs rejected when allowlist is configured
- Invalid/templated URLs allowed when no allowlist (null/undefined/[])
- Whitespace-only and empty-string URLs treated as absent across all
allowedDomains configurations
- Stdio configs (no url property) remain allowed
* 🛡️ fix: Cover full fe80::/10 link-local range in SSRF IPv6 check
The `isPrivateIP` check used `startsWith('fe80')` which only matched
fe80:: but missed fe90::–febf:: (the rest of the RFC 4291 fe80::/10
link-local block). Replace with a proper bitwise hextet check.
* 🛡️ fix: Guard isIPv6LinkLocal against parseInt partial-parse on hostnames
parseInt('fe90.example.com', 16) stops at the dot and returns 0xfe90,
which passes the bitmask check and false-positives legitimate domains.
Add colon-presence guard (IPv6 literals always contain ':') and a hex
regex validation on the first hextet before parseInt.
Also document why fc/fd use startsWith while fe80::/10 needs bitwise.
* ✅ test: Harden IPv6 link-local SSRF tests with false-positive guards
- Assert fe90/fea0/febf hostnames are NOT blocked (regression guard)
- Add feb0::1 and bracket form [fe90::1] to isPrivateIP coverage
- Extend resolveHostnameSSRF tests for fe90::1 and febf::1
SameSite=Strict cookies are withheld by browsers during cross-origin
top-level navigations (hanzo.id → hanzo.chat). This caused the
refreshToken cookie to be unavailable after the OAuth callback redirect,
making the client think the user was not authenticated.
Changed to SameSite=Lax which sends cookies on top-level navigations
while still protecting against CSRF on subresource requests.
- Clicking a suggestion types it out character by character (30ms/char)
then auto-submits after a 400ms pause
- Sign up button goes to hanzo.id/signup (not login)
- Query stored in sessionStorage for post-login redirect
* fix: emit created event from metadata on cross-replica subscribe
In multi-instance Redis deployments, the created event (which triggers
sidebar conversation creation) was lost when the SSE subscriber connected
to a different instance than the one generating. The event was only in
the generating instance's local earlyEventBuffer and the Redis pub/sub
message was already gone by the time the subscriber's channel was active.
When subscribing cross-replica (empty buffer, Redis mode, userMessage
already in job metadata), reconstruct and emit the created event
directly from stored metadata.
* test: add skipBufferReplay regression guard for cross-replica created event
Add test asserting the resume path (skipBufferReplay: true) does NOT
emit a created event on cross-replica subscribe — prevents the
duplication fix from PR #12225 from regressing. Add explanatory JSDoc
on the cross-replica fallback branch documenting which fields are
preserved from trackUserMessage() and why sender/isCreatedByUser
are hardcoded.
* refactor: replace as-unknown-as casts with discriminated ServerSentEvent union
Split ServerSentEvent into StreamEvent | CreatedEvent | FinalEvent so
event shapes are statically typed. Removes all as-unknown-as casts in
GenerationJobManager and test file; narrows with proper union members
where properties are accessed.
* fix: await trackUserMessage before PUBLISH for structural ordering
trackUserMessage was fire-and-forget — the HSET for userMessage could
theoretically race with the PUBLISH. Await it so the write commits
before the pub/sub fires, guaranteeing any cross-replica getJob() after
the pub/sub window always finds userMessage in Redis. No-op for
non-created events (early return before any async work).
* refactor: type CreatedEvent.message explicitly, fix JSDoc and import
Give CreatedEvent.message its full known shape instead of
Record<string, unknown>. Update sendEvent JSDoc to reflect the
discriminated union. Use barrel import in test file.
* refactor: type FinalEvent fields with explicit message and conversation shapes
Replace Record<string, unknown> on requestMessage, responseMessage,
conversation, and runMessages with FinalMessageFields and a typed
conversation shape. Captures the known field set used by all final
event constructors (abort handler in GenerationJobManager and normal
completion in request.js) while allowing extension via index signature
for fields contributed by the full TMessage/TConversation schemas.
* refactor: narrow trackUserMessage with discriminated union, disambiguate error fields
Use 'created' in event to narrow ServerSentEvent to CreatedEvent,
eliminating all Record<string, unknown> casts and manual field
assertions. Add JSDoc to the two distinct error fields on
FinalMessageFields and FinalEvent to prevent confusion.
* fix: update cross-replica test to expect created event from metadata
The cross-replica subscribe fallback now correctly emits a created
event reconstructed from persisted metadata when userMessage exists
in the Redis job hash. Replica B receives 4 events (created + 3
deltas) instead of 3.
* 🐛 fix: Enforce fileLimit and totalSizeLimit in Attached Files panel
The Files side panel (PanelTable) was not checking fileLimit or
totalSizeLimit from fileConfig when attaching previously uploaded files,
allowing users to bypass per-endpoint file count and total size limits.
* 🔧 fix: Address review findings on file limit enforcement
- Fix totalSizeLimit double-counting size of already-attached files
- Clarify fileLimit error message: "File limit reached: N files (endpoint)"
- Replace Array.from(...).reduce with for...of loop to avoid intermediate allocation
- Extract inline `type TFile` into standalone `import type` per project conventions
* ✅ test: Add PanelTable handleFileClick file limit tests
Cover fileLimit guard, totalSizeLimit guard, passing case,
double-count prevention for re-attached files, and boundary case.
* 🔧 test: Harden PanelTable test mock setup
- Use explicit endpoint key matching mockConversation.endpoint
instead of relying on default fallback behavior
- Add supportedMimeTypes to mock config for explicit MIME coverage
- Throw on missing filename cell in clickFilenameCell to prevent
silent false-positive blocking assertions
* ♻️ refactor: Align file validation ordering and messaging across upload paths
- Reorder handleFileClick checks to match validateFiles:
disabled → fileLimit → fileSizeLimit → checkType → totalSizeLimit
- Change fileSizeLimit comparison from > to >= in handleFileClick
to match validateFiles behavior
- Align validateFiles error strings with localized key wording:
"File limit reached:", "File size limit exceeded:", etc.
- Remove stray console.log in validateFiles MIME-type check
* ✅ test: Add validateFiles unit tests for both paths' consistency
13 tests covering disabled, empty, fileLimit (reject + boundary),
fileSizeLimit (>= at limit + under limit), checkType, totalSizeLimit
(reject + at limit), duplicate detection, and check ordering.
Ensures both validateFiles and handleFileClick enforce the same
validation rules in the same order.
* fix: respect fileConfig.disabled for agents endpoint upload button
The isAgents check was OR'd without the !isUploadDisabled guard,
bypassing the fileConfig.endpoints.agents.disabled setting and
always rendering the attach file menu for agents.
* test: add regression tests for fileConfig.disabled upload guard
Cover the isUploadDisabled rendering gate for agents and assistants
endpoints, preventing silent reintroduction of the bypass bug.
* test: cover disabled fallback chain in useAgentFileConfig
Verify agents-disabled propagates when no provider is set,
when provider has no specific config (agents as fallback),
and that provider-specific enabled overrides agents disabled.
* 🛡️ fix: Scope action mutations by parent resource ownership
Prevent cross-tenant action overwrites by validating that an existing
action's agent_id/assistant_id matches the URL parameter before allowing
updates or deletes. Without this, a user with EDIT access on their own
agent could reference a foreign action_id to hijack another agent's
action record.
* 🛡️ fix: Harden action ownership checks and scope write filters
- Remove && short-circuit that bypassed the guard when agent_id or
assistant_id was falsy (e.g. assistant-owned actions have no agent_id,
so the check was skipped entirely on the agents route).
- Include agent_id / assistant_id in the updateAction and deleteAction
query filters so the DB write itself enforces ownership atomically.
- Log a warning when deleteAction returns null (silent no-op from
data-integrity mismatch).
* 📝 docs: Update Action model JSDoc to reflect scoped query params
* ✅ test: Add Action ownership scoping tests
Cover update, delete, and cross-type protection scenarios using
MongoMemoryServer to verify that scoped query filters (agent_id,
assistant_id) prevent cross-tenant overwrites and deletions at the
database level.
* 🛡️ fix: Scope updateAction filter in agent duplication handler
* 🐛 fix: Use action metadata domain instead of action_id when duplicating agent actions
The duplicate handler was splitting `action.action_id` by `actionDelimiter`
to extract the domain, but `action_id` is a bare nanoid that doesn't
contain the delimiter. This produced malformed entries in the duplicated
agent's actions array (nanoid_action_newNanoid instead of
domain_action_newNanoid). The domain is available on `action.metadata.domain`.
* ✅ test: Add integration tests for agent duplication action handling
Uses MongoMemoryServer with real Agent and Action models to verify:
- Duplicated actions use metadata.domain (not action_id) for the
agent actions array entries
- Sensitive metadata fields are stripped from duplicated actions
- Original action documents are not modified
* 🐛 fix: Normalize non-standard browser MIME types in inferMimeType
macOS Chrome/Firefox report .py files as text/x-python-script instead
of text/x-python, causing client-side validation to reject Python file
uploads. inferMimeType now normalizes known MIME type aliases before
returning, so non-standard variants match the accepted regex patterns.
* 🧪 test: Add tests for MIME type alias normalization in inferMimeType
* 🐛 fix: Restore JSDoc params and make mimeTypeAliases immutable
* 🧪 test: Add checkType integration tests, remove redundant DragDropModal tests
* refactor: Better UX for MCP stdio with Custom User Variables
- Updated the ConnectionsRepository to prevent connections when customUserVars are defined, improving security and access control.
- Modified the MCPServerInspector to skip capabilities fetch when customUserVars are present, streamlining server inspection.
- Added tests to validate connection restrictions with customUserVars, ensuring robust handling of various server configurations.
This change enhances the overall integrity of the connection management process by enforcing stricter rules around custom user variables.
* fix: guard against empty customUserVars and add JSDoc context
- Extract `hasCustomUserVars()` helper to guard against truthy `{}`
(Zod's `.record().optional()` yields `{}` on empty input, not `undefined`)
- Add JSDoc to `isAllowedToConnectToServer` explaining why customUserVars
servers are excluded from app-level connections
* test: improve customUserVars test coverage and fixture hygiene
- Add no-connection-provided test for MCPServerInspector (production path)
- Fix test descriptions to match actual fixture values
- Replace real package name with fictional @test/mcp-stdio-server
* fix: skipBufferReplay for job resume connections
- Introduced a new option `skipBufferReplay` in the `subscribe` method of `GenerationJobManagerClass` to prevent duplication of events when resuming a connection.
- Updated the logic to conditionally skip replaying buffered events if a sync event has already been sent, enhancing the efficiency of event handling during reconnections.
- Added integration tests to verify the correct behavior of the new option, ensuring that no buffered events are replayed when `skipBufferReplay` is true, while still allowing for normal replay behavior when false.
* refactor: Update GenerationJobManager to handle sync events more efficiently
- Modified the `subscribe` method to utilize a new `skipBufferReplay` option, allowing for the prevention of duplicate events during resume connections.
- Enhanced the logic in the `chat/stream` route to conditionally skip replaying buffered events if a sync event has already been sent, improving event handling efficiency.
- Updated integration tests to verify the correct behavior of the new option, ensuring that no buffered events are replayed when `skipBufferReplay` is true, while maintaining normal replay behavior when false.
* test: Enhance GenerationJobManager integration tests for Redis mode
- Updated integration tests to conditionally run based on the USE_REDIS environment variable, allowing for better control over Redis-related tests.
- Refactored test descriptions to utilize a dynamic `describeRedis` function, improving clarity and organization of tests related to Redis functionality.
- Removed redundant checks for Redis availability within individual tests, streamlining the test logic and enhancing readability.
* fix: sync handler state for new messages on resume
The sync event's else branch (new response message) was missing
resetContentHandler() and syncStepMessage() calls, leaving stale
handler state that caused subsequent deltas to build on partial
content instead of the synced aggregatedContent.
* feat: atomic subscribeWithResume to close resume event gap
Replaces separate getResumeState() + subscribe() calls with a single
subscribeWithResume() that atomically drains earlyEventBuffer between
the resume snapshot and the subscribe. In in-memory mode, drained events
are returned as pendingEvents for the client to replay after sync.
In Redis mode, pendingEvents is empty since chunks are already persisted.
The route handler now uses the atomic method for resume connections and
extracted shared SSE write helpers to reduce duplication. The client
replays any pendingEvents through the existing step/content handlers
after applying aggregatedContent from the sync payload.
* fix: only capture gap events in subscribeWithResume, not pre-snapshot buffer
The previous implementation drained the entire earlyEventBuffer into
pendingEvents, but pre-snapshot events are already reflected in
aggregatedContent. Replaying them re-introduced the duplication bug
through a different vector.
Now records buffer length before getResumeState() and slices from that
index, so only events arriving during the async gap are returned as
pendingEvents.
Also:
- Handle pendingEvents when resumeState is null (replay directly)
- Hoist duplicate test helpers to shared scope
- Remove redundant writableEnded guard in onDone
- Remove all red (#fd4444) — pure monochrome matching hanzo.ai
- Animated logo with 3D rotateY spin + wordmark show/hide on hover
(same pattern as hanzo.ai Logo.tsx)
- Real search input (stores query, redirects to login)
- Suggestion buttons fill the input on click
- Architectural grid background + radial glow pulse
- Staggered fade-up animations
- Model badges: Zen 4, Claude, GPT-5, DeepSeek, Gemini, Qwen
- Minimal footer: hanzo.ai link, free credit CTA, docs
Replace the full marketing page with a clean, minimal landing inspired
by chatgpt.com:
- Big centered "What can I help with?" heading
- Pill-shaped chat input (links to login)
- 4 contextual suggestion prompts
- Model badges (Zen 4, Claude, GPT-5, etc.)
- Minimal navbar with logo, login, signup
- Dark theme aligned with @hanzo/ui tokens
* 🔧 chore: Update file-type dependency to version 21.3.2 in package-lock.json and package.json
- Upgraded the "file-type" package from version 18.7.0 to 21.3.2 to ensure compatibility with the latest features and security updates.
- Added new dependencies related to the updated "file-type" package, enhancing functionality and performance.
* 🔧 chore: Upgrade undici dependency to version 7.24.1 in package-lock.json and package.json
- Updated the "undici" package from version 7.18.2 to 7.24.1 across multiple package files to ensure compatibility with the latest features and security updates.
* 🔧 chore: Upgrade yauzl dependency to version 3.2.1 in package-lock.json
- Updated the "yauzl" package from version 3.2.0 to 3.2.1 to incorporate the latest features and security updates.
* 🔧 chore: Upgrade hono dependency to version 4.12.7 in package-lock.json
- Updated the "hono" package from version 4.12.5 to 4.12.7 to incorporate the latest features and security updates.
* fix: sanitize artifact filenames to prevent path traversal in code output
* test: Mock sanitizeFilename function in process.spec.js to return the original filename
- Added a mock implementation for the `sanitizeFilename` function in the `process.spec.js` test file to return the original filename, ensuring that tests can run without altering the filename during the testing process.
* fix: use path.relative for traversal check, sanitize all filenames, add security logging
- Replace startsWith with path.relative pattern in saveLocalBuffer, consistent
with deleteLocalFile and getLocalFileStream in the same file
- Hoist sanitizeFilename call before the image/non-image branch so both code
paths store the sanitized name in MongoDB
- Log a warning when sanitizeFilename mutates a filename (potential traversal)
- Log a specific warning when saveLocalBuffer throws a traversal error, so
security events are distinguishable from generic network errors in the catch
* test: improve traversal test coverage and remove mock reimplementation
- Remove partial sanitizeFilename reimplementation from process-traversal tests;
use controlled mock returns to verify processCodeOutput wiring instead
- Add test for image branch sanitization
- Use mkdtempSync for test isolation in crud-traversal to avoid parallel worker
collisions
- Add prefix-collision bypass test case (../user10/evil vs user1 directory)
* fix: use path.relative in isValidPath to prevent prefix-collision bypass
Pre-existing startsWith check without path separator had the same class
of prefix-collision vulnerability fixed in saveLocalBuffer.
* fix: add file size limits to conversation import multer instance
* fix: address review findings for conversation import file size limits
* fix: use local jest.mock for data-schemas instead of global moduleNameMapper
The global @librechat/data-schemas mock in jest.config.js only provided
logger, breaking all tests that depend on createModels from the same
package. Replace with a virtual jest.mock scoped to the import spec file.
* fix: move import to top of file, pre-compute upload middleware, assert logger.warn in tests
* refactor: move resolveImportMaxFileSize to packages/api
New backend logic belongs in packages/api as TypeScript. Delete the
api/server/utils/import/limits.js wrapper and import directly from
@librechat/api in convos.js and importConversations.js. Resolver unit
tests move to packages/api; the api/ spec retains only multer behavior
tests.
* chore: rename importLimits to import
* fix: stale type reference and mock isolation in import tests
Update typeof import path from '../importLimits' to '../import' after
the rename. Clear mockLogger.warn in beforeEach to prevent cross-test
accumulation.
* fix: add resolveImportMaxFileSize to @librechat/api mock in convos.spec.js
* fix: resolve jest.mock hoisting issue in import tests
jest.mock factories are hoisted above const declarations, so the
mockLogger reference was undefined at factory evaluation time. Use a
direct import of the mocked logger module instead.
* fix: remove virtual flag from data-schemas mock for CI compatibility
virtual: true prevents the mock from intercepting the real module in
CI where @librechat/data-schemas is built, causing import.ts to use
the real logger while the test asserts against the mock.
* fix: add agent permission check to image upload route
* refactor: remove unused SystemRoles import and format test file for clarity
* fix: address review findings for image upload agent permission check
* refactor: move agent upload auth logic to TypeScript in packages/api
Extract pure authorization logic from agentPermCheck.js into
checkAgentUploadAuth() in packages/api/src/files/agentUploadAuth.ts.
The function returns a structured result ({ allowed, status, error })
instead of writing HTTP responses directly, eliminating the dual
responsibility and confusing sentinel return value. The JS wrapper
in /api is now a thin adapter that translates the result to HTTP.
* test: rewrite image upload permission tests as integration tests
Replace mock-heavy images-agent-perm.spec.js with integration tests
using MongoMemoryServer, real models, and real PermissionService.
Follows the established pattern in files.agents.test.js. Moves test
to sibling location (images.agents.test.js) matching backend convention.
Adds temp file cleanup assertions on 403/404 responses and covers
message_file exemption paths (boolean true, string "true", false).
* fix: widen AgentUploadAuthDeps types to accept ObjectId from Mongoose
The injected getAgent returns Mongoose documents where _id and author
are Types.ObjectId at runtime, not string. Widen the DI interface to
accept string | Types.ObjectId for _id, author, and resourceId so the
contract accurately reflects real callers.
* chore: move agent upload auth into files/agents/ subdirectory
* refactor: delete agentPermCheck.js wrapper, move verifyAgentUploadPermission to packages/api
The /api-only dependencies (getAgent, checkPermission) are now passed
as object-field params from the route call sites. Both images.js and
files.js import verifyAgentUploadPermission from @librechat/api and
inject the deps directly, eliminating the intermediate JS wrapper.
* style: fix import type ordering in agent upload auth
* fix: prevent token TTL race in MCPTokenStorage.storeTokens
When expires_in is provided, use it directly instead of round-tripping
through Date arithmetic. The previous code computed accessTokenExpiry
as a Date, then after an async encryptV2 call, recomputed expiresIn by
subtracting Date.now(). On loaded CI runners the elapsed time caused
Math.floor to truncate to 0, triggering the 1-year fallback and making
the token appear permanently valid — so refresh never fired.
* fix: require OTP verification for 2FA re-enrollment and backup code regeneration
* fix: require OTP verification for account deletion when 2FA is enabled
* refactor: Improve code formatting and readability in TwoFactorController and UserController
- Reformatted code in TwoFactorController and UserController for better readability by aligning parameters and breaking long lines.
- Updated test cases in deleteUser.spec.js and TwoFactorController.spec.js to enhance clarity by formatting object parameters consistently.
* refactor: Consolidate OTP and backup code verification logic in TwoFactorController and UserController
- Introduced a new `verifyOTPOrBackupCode` function to streamline the verification process for TOTP tokens and backup codes across multiple controllers.
- Updated the `enable2FA`, `disable2FA`, and `deleteUserController` methods to utilize the new verification function, enhancing code reusability and readability.
- Adjusted related tests to reflect the changes in verification logic, ensuring consistent behavior across different scenarios.
- Improved error handling and response messages for verification failures, providing clearer feedback to users.
* chore: linting
* refactor: Update BackupCodesItem component to enhance OTP verification logic
- Consolidated OTP input handling by moving the 2FA verification UI logic to a more consistent location within the component.
- Improved the state management for OTP readiness, ensuring the regenerate button is only enabled when the OTP is ready.
- Cleaned up imports by removing redundant type imports, enhancing code clarity and maintainability.
* chore: lint
* fix: stage 2FA re-enrollment in pending fields to prevent disarmament window
enable2FA now writes to pendingTotpSecret/pendingBackupCodes instead of
overwriting the live fields. confirm2FA performs the atomic swap only after
the new TOTP code is verified. If the user abandons mid-flow, their
existing 2FA remains active and intact.
* fix: add user filter to message deletion to prevent IDOR
* refactor: streamline DELETE request syntax in messages-delete test
- Simplified the DELETE request syntax in the messages-delete.spec.js test file by combining multiple lines into a single line for improved readability. This change enhances the clarity of the test code without altering its functionality.
* fix: address review findings for message deletion IDOR fix
* fix: add user filter to message deletion in conversation tests
- Included a user filter in the message deletion test to ensure proper handling of user-specific deletions, enhancing the accuracy of the test case and preventing potential IDOR vulnerabilities.
* chore: lint
* fix: add rate limiting to conversation duplicate endpoint
* chore: linter
* fix: address review findings for conversation duplicate rate limiting
* refactor: streamline test mocks for conversation routes
- Consolidated mock implementations into a dedicated `convos-route-mocks.js` file to enhance maintainability and readability of test files.
- Updated tests in `convos-duplicate-ratelimit.spec.js` and `convos.spec.js` to utilize the new mock structure, improving clarity and reducing redundancy.
- Enhanced the `duplicateConversation` function to accept an optional title parameter for better flexibility in conversation duplication.
* chore: rename files
* 🔒 fix: Validate MCP Configs in Server Responses
* 🔒 fix: Enhance OAuth URL Validation in MCPOAuthHandler
- Introduced validation for OAuth URLs to ensure they do not target private or internal addresses, enhancing security against SSRF attacks.
- Updated the OAuth flow to validate both authorization and token URLs before use, ensuring compliance with security standards.
- Refactored redirect URI handling to streamline the OAuth client registration process.
- Added comprehensive error handling for invalid URLs, improving robustness in OAuth interactions.
* 🔒 feat: Implement Permission Checks for MCP Server Management
- Added permission checkers for MCP server usage and creation, enhancing access control.
- Updated routes for reinitializing MCP servers and retrieving authentication values to include these permission checks, ensuring only authorized users can access these functionalities.
- Refactored existing permission logic to improve clarity and maintainability.
* 🔒 fix: Enhance MCP Server Response Validation and Redaction
- Updated MCP route tests to use `toMatchObject` for better validation of server response structures, ensuring consistency in expected properties.
- Refactored the `redactServerSecrets` function to streamline the removal of sensitive information, ensuring that user-sourced API keys are properly redacted while retaining their source.
- Improved OAuth security tests to validate rejection of private URLs across multiple endpoints, enhancing protection against SSRF vulnerabilities.
- Added comprehensive tests for the `redactServerSecrets` function to ensure proper handling of various server configurations, reinforcing security measures.
* chore: eslint
* 🔒 fix: Enhance OAuth Server URL Validation in MCPOAuthHandler
- Added validation for discovered authorization server URLs to ensure they meet security standards.
- Improved logging to provide clearer insights when an authorization server is found from resource metadata.
- Refactored the handling of authorization server URLs to enhance robustness against potential security vulnerabilities.
* 🔒 test: Bypass SSRF validation for MCP OAuth Flow tests
- Mocked SSRF validation functions to allow tests to use real local HTTP servers, facilitating more accurate testing of the MCP OAuth flow.
- Updated test setup to ensure compatibility with the new mocking strategy, enhancing the reliability of the tests.
* 🔒 fix: Add Validation for OAuth Metadata Endpoints in MCPOAuthHandler
- Implemented checks for the presence and validity of registration and token endpoints in the OAuth metadata, enhancing security by ensuring that these URLs are properly validated before use.
- Improved error handling and logging to provide better insights during the OAuth metadata processing, reinforcing the robustness of the OAuth flow.
* 🔒 refactor: Simplify MCP Auth Values Endpoint Logic
- Removed redundant permission checks for accessing the MCP server resource in the auth-values endpoint, streamlining the request handling process.
- Consolidated error handling and response structure for improved clarity and maintainability.
- Enhanced logging for better insights during the authentication value checks, reinforcing the robustness of the endpoint.
* 🔒 test: Refactor LeaderElection Integration Tests for Improved Cleanup
- Moved Redis key cleanup to the beforeEach hook to ensure a clean state before each test.
- Enhanced afterEach logic to handle instance resignations and Redis key deletion more robustly, improving test reliability and maintainability.
* fix: MCP server configuration validation and schema
- Added tests to reject URLs containing environment variable references for SSE, streamable-http, and websocket types in the MCP routes.
- Introduced a new schema in the data provider to ensure user input URLs do not resolve environment variables, enhancing security against potential leaks.
- Updated existing MCP server user input schema to utilize the new validation logic, ensuring consistent handling of user-supplied URLs across the application.
* fix: MCP URL validation to reject env variable references
- Updated tests to ensure that URLs for SSE, streamable-http, and websocket types containing environment variable patterns are rejected, improving security against potential leaks.
- Refactored the MCP server user input schema to enforce stricter validation rules, preventing the resolution of environment variables in user-supplied URLs.
- Introduced new test cases for various URL types to validate the rejection logic, ensuring consistent handling across the application.
* test: Enhance MCPServerUserInputSchema tests for environment variable handling
- Introduced new test cases to validate the prevention of environment variable exfiltration through user input URLs in the MCPServerUserInputSchema.
- Updated existing tests to confirm that URLs containing environment variable patterns are correctly resolved or rejected, improving security against potential leaks.
- Refactored test structure to better organize environment variable handling scenarios, ensuring comprehensive coverage of edge cases.
* fix: meili index sync with unindexed documents
- Updated `performSync` function to force a full sync when a fresh MeiliSearch index is detected, even if the number of unindexed messages or convos is below the sync threshold.
- Added logging to indicate when a fresh index is detected and a full sync is initiated.
- Introduced new tests to validate the behavior of the sync logic under various conditions, ensuring proper handling of fresh indexes and threshold scenarios.
This change improves the reliability of the synchronization process, ensuring that all documents are indexed correctly when starting with a fresh index.
* refactor: update sync logic for unindexed documents in MeiliSearch
- Renamed variables in `performSync` to improve clarity, changing `freshIndex` to `noneIndexed` for better understanding of the sync condition.
- Adjusted the logic to ensure a full sync is forced when no messages or conversations are marked as indexed, even if below the sync threshold.
- Updated related tests to reflect the new logging messages and conditions, enhancing the accuracy of the sync threshold logic.
This change improves the readability and reliability of the synchronization process, ensuring all documents are indexed correctly when starting with a fresh index.
* fix: enhance MeiliSearch index creation error handling
- Updated the `mongoMeili` function to improve logging and error handling during index creation in MeiliSearch.
- Added handling for `MeiliSearchTimeOutError` to log a warning when index creation times out.
- Enhanced logging to differentiate between successful index creation and specific failure reasons, including cases where the index already exists.
- Improved debug logging for index creation tasks to provide clearer insights into the process.
This change enhances the robustness of the index creation process and improves observability for troubleshooting.
* fix: update MeiliSearch index creation error handling
- Modified the `mongoMeili` function to check for any status other than 'succeeded' during index creation, enhancing error detection.
- Improved logging to provide clearer insights when an index creation task fails, particularly for cases where the index already exists.
This change strengthens the error handling mechanism for index creation in MeiliSearch, ensuring better observability and reliability.
Replace plaintext Anthropic API key, JWT secrets, credential keys,
and Meilisearch master key with placeholders. Add .env.hanzo to
.gitignore to prevent future secret commits.
* chore: Update dependencies by adding ai-tokenizer and removing tiktoken
- Added ai-tokenizer version 1.0.6 to package.json and package-lock.json across multiple packages.
- Removed tiktoken version 1.0.15 from package.json and package-lock.json in the same locations, streamlining dependency management.
* refactor: replace js-tiktoken with ai-tokenizer
- Added support for 'claude' encoding in the AgentClient class to improve model compatibility.
- Updated Tokenizer class to utilize 'ai-tokenizer' for both 'o200k_base' and 'claude' encodings, replacing the previous 'tiktoken' dependency.
- Refactored tests to reflect changes in tokenizer behavior and ensure accurate token counting for both encoding types.
- Removed deprecated references to 'tiktoken' and adjusted related tests for improved clarity and functionality.
* chore: remove tiktoken mocks from DALLE3 tests
- Eliminated mock implementations of 'tiktoken' from DALLE3-related test files to streamline test setup and align with recent dependency updates.
- Adjusted related test structures to ensure compatibility with the new tokenizer implementation.
* chore: Add distinct encoding support for Anthropic Claude models
- Introduced a new method `getEncoding` in the AgentClient class to handle the specific BPE tokenizer for Claude models, ensuring compatibility with the distinct encoding requirements.
- Updated documentation to clarify the encoding logic for Claude and other models.
* docs: Update return type documentation for getEncoding method in AgentClient
- Clarified the return type of the getEncoding method to specify that it can return an EncodingName or undefined, enhancing code readability and type safety.
* refactor: Tokenizer class and error handling
- Exported the EncodingName type for broader usage.
- Renamed encodingMap to encodingData for clarity.
- Improved error handling in getTokenCount method to ensure recovery attempts are logged and return 0 on failure.
- Updated countTokens function documentation to specify the use of 'o200k_base' encoding.
* refactor: Simplify encoding documentation and export type
- Updated the getEncoding method documentation to clarify the default behavior for non-Anthropic Claude models.
- Exported the EncodingName type separately from the Tokenizer module for improved clarity and usage.
* test: Update text processing tests for token limits
- Adjusted test cases to handle smaller text sizes, changing scenarios from ~120k tokens to ~20k tokens for both the real tokenizer and countTokens functions.
- Updated token limits in tests to reflect new constraints, ensuring tests accurately assess performance and call reduction.
- Enhanced console log messages for clarity regarding token counts and reductions in the updated scenarios.
* refactor: Update Tokenizer imports and exports
- Moved Tokenizer and countTokens exports to the tokenizer module for better organization.
- Adjusted imports in memory.ts to reflect the new structure, ensuring consistent usage across the codebase.
- Updated memory.test.ts to mock the Tokenizer from the correct module path, enhancing test accuracy.
* refactor: Tokenizer initialization and error handling
- Introduced an async `initEncoding` method to preload tokenizers, improving performance and accuracy in token counting.
- Updated `getTokenCount` to handle uninitialized tokenizers more gracefully, ensuring proper recovery and logging on errors.
- Removed deprecated synchronous tokenizer retrieval, streamlining the overall tokenizer management process.
* test: Enhance tokenizer tests with initialization and encoding checks
- Added `beforeAll` hooks to initialize tokenizers for 'o200k_base' and 'claude' encodings before running tests, ensuring proper setup.
- Updated tests to validate the loading of encodings and the correctness of token counts for both 'o200k_base' and 'claude'.
- Improved test structure to deduplicate concurrent initialization calls, enhancing performance and reliability.
* fix: Implement race conditions in MCP OAuth flow
- Added connection mutex to coalesce concurrent `getUserConnection` calls, preventing multiple simultaneous attempts.
- Enhanced flow state management to retry once when a flow state is missing, improving resilience against race conditions.
- Introduced `ReauthenticationRequiredError` for better error handling when access tokens are expired or missing.
- Updated tests to cover new race condition scenarios and ensure proper handling of OAuth flows.
* fix: Stale PENDING flow detection and OAuth URL re-issuance
PENDING flows in handleOAuthRequired now check createdAt age — flows
older than 2 minutes are treated as stale and replaced instead of
joined. Fixes the case where a leftover PENDING flow from a previous
session blocks new OAuth initiation.
authorizationUrl is now stored in MCPOAuthFlowMetadata so that when a
second caller joins an active PENDING flow (e.g., the SSE-emitting path
in ToolService), it can re-issue the URL to the user via oauthStart.
* fix: CSRF fallback via active PENDING flow in OAuth callback
When the OAuth callback arrives without CSRF or session cookies (common
in the chat/SSE flow where cookies can't be set on streaming responses),
fall back to validating that a PENDING flow exists for the flowId. This
is safe because the flow was created server-side after JWT authentication
and the authorization code is PKCE-protected.
* test: Extract shared OAuth test server helpers
Move MockKeyv, getFreePort, trackSockets, and createOAuthMCPServer into
a shared helpers/oauthTestServer module. Enhance the test server with
refresh token support, token rotation, metadata discovery, and dynamic
client registration endpoints. Add InMemoryTokenStore for token storage
tests.
Refactor MCPOAuthRaceCondition.test.ts to import from shared helpers.
* test: Add comprehensive MCP OAuth test modules
MCPOAuthTokenStorage — 21 tests for storeTokens/getTokens with
InMemoryTokenStore: encrypt/decrypt round-trips, expiry calculation,
refresh callback wiring, ReauthenticationRequiredError paths.
MCPOAuthFlow — 10 tests against real HTTP server: token refresh with
stored client info, refresh token rotation, metadata discovery, dynamic
client registration, full store/retrieve/expire/refresh lifecycle.
MCPOAuthConnectionEvents — 5 tests for MCPConnection OAuth event cycle
with real OAuth-gated MCP server: oauthRequired emission on 401,
oauthHandled reconnection, oauthFailed rejection, token expiry detection.
MCPOAuthTokenExpiry — 12 tests for the token expiry edge case: refresh
success/failure paths, ReauthenticationRequiredError, PENDING flow CSRF
fallback, authorizationUrl metadata storage, full re-auth cycle after
refresh failure, concurrent expired token coalescing, stale PENDING
flow detection.
* test: Enhance MCP OAuth connection tests with cooldown reset
Added a `beforeEach` hook to clear the cooldown for `MCPConnection` before each test, ensuring a clean state. Updated the race condition handling in the tests to properly clear the timeout, improving reliability in the event data retrieval process.
* refactor: PENDING flow management and state recovery in MCP OAuth
- Introduced a constant `PENDING_STALE_MS` to define the age threshold for PENDING flows, improving the handling of stale flows.
- Updated the logic in `MCPConnectionFactory` and `FlowStateManager` to check the age of PENDING flows before joining or reusing them.
- Modified the `completeFlow` method to return false when the flow state is deleted, ensuring graceful handling of race conditions.
- Enhanced tests to validate the new behavior and ensure robustness against state recovery issues.
* refactor: MCP OAuth flow management and testing
- Updated the `completeFlow` method to log warnings when a tool flow state is not found during completion, improving error handling.
- Introduced a new `normalizeExpiresAt` function to standardize expiration timestamp handling across the application.
- Refactored token expiration checks in `MCPConnectionFactory` to utilize the new normalization function, ensuring consistent behavior.
- Added a comprehensive test suite for OAuth callback CSRF fallback logic, validating the handling of PENDING flows and their staleness.
- Enhanced existing tests to cover new expiration normalization logic and ensure robust flow state management.
* test: Add CSRF fallback tests for active PENDING flows in MCP OAuth
- Introduced new tests to validate CSRF fallback behavior when a fresh PENDING flow exists without cookies, ensuring successful OAuth callback handling.
- Added scenarios to reject requests when no PENDING flow exists, when only a COMPLETED flow is present, and when a PENDING flow is stale, enhancing the robustness of flow state management.
- Improved overall test coverage for OAuth callback logic, reinforcing the handling of CSRF validation failures.
* chore: imports order
* refactor: Update UserConnectionManager to conditionally manage pending connections
- Modified the logic in `UserConnectionManager` to only set pending connections if `forceNew` is false, preventing unnecessary overwrites.
- Adjusted the cleanup process to ensure pending connections are only deleted when not forced, enhancing connection management efficiency.
* refactor: MCP OAuth flow state management
- Introduced a new method `storeStateMapping` in `MCPOAuthHandler` to securely map the OAuth state parameter to the flow ID, improving callback resolution and security against forgery.
- Updated the OAuth initiation and callback handling in `mcp.js` to utilize the new state mapping functionality, ensuring robust flow management.
- Refactored `MCPConnectionFactory` to store state mappings during flow initialization, enhancing the integrity of the OAuth process.
- Adjusted comments to clarify the purpose of state parameters in authorization URLs, reinforcing code readability.
* refactor: MCPConnection with OAuth recovery handling
- Added `oauthRecovery` flag to manage OAuth recovery state during connection attempts.
- Introduced `decrementCycleCount` method to reduce the circuit breaker's cycle count upon successful reconnection after OAuth recovery.
- Updated connection logic to reset the `oauthRecovery` flag after handling OAuth, improving state management and connection reliability.
* chore: Add debug logging for OAuth recovery cycle count decrement
- Introduced a debug log statement in the `MCPConnection` class to track the decrement of the cycle count after a successful reconnection during OAuth recovery.
- This enhancement improves observability and aids in troubleshooting connection issues related to OAuth recovery.
* test: Add OAuth recovery cycle management tests
- Introduced new tests for the OAuth recovery cycle in `MCPConnection`, validating the decrement of cycle counts after successful reconnections.
- Added scenarios to ensure that the cycle count is not decremented on OAuth failures, enhancing the robustness of connection management.
- Improved test coverage for OAuth reconnect scenarios, ensuring reliable behavior under various conditions.
* feat: Implement circuit breaker configuration in MCP
- Added circuit breaker settings to `.env.example` for max cycles, cycle window, and cooldown duration.
- Refactored `MCPConnection` to utilize the new configuration values from `mcpConfig`, enhancing circuit breaker management.
- Improved code maintainability by centralizing circuit breaker parameters in the configuration file.
* refactor: Update decrementCycleCount method for circuit breaker management
- Changed the visibility of the `decrementCycleCount` method in `MCPConnection` from private to public static, allowing it to be called with a server name parameter.
- Updated calls to `decrementCycleCount` in `MCPConnectionFactory` to use the new static method, improving clarity and consistency in circuit breaker management during connection failures and OAuth recovery.
- Enhanced the handling of circuit breaker state by ensuring the method checks for the existence of the circuit breaker before decrementing the cycle count.
* refactor: cycle count decrement on tool listing failure
- Added a call to `MCPConnection.decrementCycleCount` in the `MCPConnectionFactory` to handle cases where unauthenticated tool listing fails, improving circuit breaker management.
- This change ensures that the cycle count is decremented appropriately, maintaining the integrity of the connection recovery process.
* refactor: Update circuit breaker configuration and logic
- Enhanced circuit breaker settings in `.env.example` to include new parameters for failed rounds and backoff strategies.
- Refactored `MCPConnection` to utilize the updated configuration values from `mcpConfig`, improving circuit breaker management.
- Updated tests to reflect changes in circuit breaker logic, ensuring accurate validation of connection behavior under rapid reconnect scenarios.
* feat: Implement state mapping deletion in MCP flow management
- Added a new method `deleteStateMapping` in `MCPOAuthHandler` to remove orphaned state mappings when a flow is replaced, preventing old authorization URLs from resolving after a flow restart.
- Updated `MCPConnectionFactory` to call `deleteStateMapping` during flow cleanup, ensuring proper management of OAuth states.
- Enhanced test coverage for state mapping functionality to validate the new deletion logic.
Adds @hanzo/ui dependency and ChatHanzoHeader component to Root.tsx.
Renders unified Hanzo header bar with app switcher and user dropdown
above existing chat UI. Uses require() pattern for CJS compat.
Replace all chat.hanzo.ai references with hanzo.chat across source,
config, and documentation. The old domain still serves via Traefik/K8s
but will 301 redirect to hanzo.chat.
* 🧪 test: Add reconnection storm regression tests for MCPConnection
Introduced a comprehensive test suite for reconnection storm scenarios, validating circuit breaker, throttling, cooldown, and timeout fixes. The tests utilize real MCP SDK transports and a StreamableHTTP server to ensure accurate behavior under rapid connect/disconnect cycles and error handling for SSE 400/405 responses. This enhances the reliability of the MCPConnection by ensuring proper handling of reconnection logic and circuit breaker functionality.
* 🔧 fix: Update createUnavailableToolStub to return structured response
Modified the `createUnavailableToolStub` function to return an array containing the unavailable message and a null value, enhancing the response structure. Additionally, added a debug log to skip tool creation when the result is null, improving the handling of reconnection scenarios in the MCP service.
* 🧪 test: Enhance MCP tool creation tests for cache and throttle interactions
Added new test cases for the `createMCPTool` function to validate the caching behavior when tools are unavailable or throttled. The tests ensure that tools are correctly cached as missing and prevent unnecessary reconnects across different users, improving the reliability of the MCP service under concurrent usage scenarios. Additionally, introduced a test for the `createMCPTools` function to verify that it returns an empty array when reconnect is throttled, ensuring proper handling of throttling logic.
* 📝 docs: Update AGENTS.md with testing philosophy and guidelines
Expanded the testing section in AGENTS.md to emphasize the importance of using real logic over mocks, advocating for the use of spies and real dependencies in tests. Added specific recommendations for testing with MongoDB and MCP SDK, highlighting the need to mock only uncontrollable external services. This update aims to improve testing practices and encourage more robust test implementations.
* 🧪 test: Enhance reconnection storm tests with socket tracking and SSE handling
Updated the reconnection storm test suite to include a new socket tracking mechanism for better resource management during tests. Improved the handling of SSE 400/405 responses by ensuring they are processed in the same branch as 404 errors, preventing unhandled cases. This enhances the reliability of the MCPConnection under rapid reconnect scenarios and ensures proper error handling.
* 🔧 fix: Implement cache eviction for stale reconnect attempts and missing tools
Added an `evictStale` function to manage the size of the `lastReconnectAttempts` and `missingToolCache` maps, ensuring they do not exceed a maximum cache size. This enhancement improves resource management by removing outdated entries based on a specified time-to-live (TTL), thereby optimizing the MCP service's performance during reconnection scenarios.
* 🔄 refactor: OAuth Metadata Discovery with Origin Fallback
Updated the `discoverWithOriginFallback` method to improve the handling of OAuth authorization server metadata discovery. The method now retries with the origin URL when discovery fails for a path-based URL, ensuring consistent behavior across `discoverMetadata` and token refresh flows. This change reduces code duplication and enhances the reliability of the OAuth flow by providing a unified implementation for origin fallback logic.
* 🧪 test: Add tests for OAuth Token Refresh with Origin Fallback
Introduced new tests for the `refreshOAuthTokens` method in `MCPOAuthHandler` to validate the retry mechanism with the origin URL when path-based discovery fails. The tests cover scenarios where the first discovery attempt throws an error and the subsequent attempt succeeds, as well as cases where the discovery fails entirely. This enhances the reliability of the OAuth token refresh process by ensuring proper handling of discovery failures.
* chore: imports order
* fix: Improve Base URL Logging and Metadata Discovery in MCPOAuthHandler
Updated the logging to use a consistent base URL object when handling discovery failures in the MCPOAuthHandler. This change enhances error reporting by ensuring that the base URL is logged correctly, and it refines the metadata discovery process by returning the result of the discovery attempt with the base URL, improving the reliability of the OAuth flow.
* fix: add base URL fallback for path-based OAuth discovery in token refresh
The two `refreshOAuthTokens` paths in `MCPOAuthHandler` were missing the
origin-URL fallback that `initiateOAuthFlow` already had. With MCP SDK
1.27.1, `buildDiscoveryUrls` appends the server path to the
`.well-known` URL (e.g. `/.well-known/oauth-authorization-server/mcp`),
which returns 404 for servers like Sentry that only expose the root
discovery endpoint (`/.well-known/oauth-authorization-server`).
Without the fallback, discovery returns null during refresh, the token
endpoint resolves to the wrong URL, and users are prompted to
re-authenticate every time their access token expires instead of the
refresh token being exchanged silently.
Both refresh paths now mirror the `initiateOAuthFlow` pattern: if
discovery fails and the server URL has a non-root path, retry with just
the origin URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: extract discoverWithOriginFallback helper; add tests
Extract the duplicated path-based URL retry logic from both
`refreshOAuthTokens` branches into a single private static helper
`discoverWithOriginFallback`, reducing the risk of the two paths
drifting in the future.
Add three tests covering the new behaviour:
- stored clientInfo path: asserts discovery is called twice (path then
origin) and that the token endpoint from the origin discovery is used
- auto-discovered path: same assertions for the branchless path
- root URL: asserts discovery is called only once when the server URL
already has no path component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: use discoverWithOriginFallback in discoverMetadata too
Remove the inline duplicate of the origin-fallback logic from
`discoverMetadata` and replace it with a call to the shared
`discoverWithOriginFallback` helper, giving all three discovery
sites a single implementation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: use mock.calls + .href/.toString() for URL assertions
Replace brittle `toHaveBeenNthCalledWith(new URL(...))` comparisons
with `expect.any(URL)` matchers and explicit `.href`/`.toString()`
checks on the captured call args, consistent with the existing
mock.calls pattern used throughout handler.test.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* ✨ v0.8.3
* chore: Bump package versions and update configuration
- Updated package versions for @librechat/api (1.7.25), @librechat/client (0.4.54), librechat-data-provider (0.8.302), and @librechat/data-schemas (0.0.38).
- Incremented configuration version in librechat.example.yaml to 1.3.6.
* feat: Add OpenRouter headers to OpenAI configuration
- Introduced 'X-OpenRouter-Title' and 'X-OpenRouter-Categories' headers in the OpenAI configuration for enhanced compatibility with OpenRouter services.
- Updated related tests to ensure the new headers are correctly included in the configuration responses.
* chore: Update package versions and dependencies
- Bumped versions for several dependencies including @eslint/eslintrc to 3.3.4, axios to 1.13.5, express to 5.2.1, and lodash to 4.17.23.
- Updated @librechat/backend and @librechat/frontend versions to 0.8.3.
- Added new dependencies: turbo and mammoth.
- Adjusted various other dependencies to their latest versions for improved compatibility and performance.
* 📦 chore: bump `mermaid` and `dompurify`
- Bump mermaid to version 11.13.0 in both package-lock.json and client/package.json.
- Update monaco-editor to version 0.55.1 in both package-lock.json and client/package.json.
- Upgrade @chevrotain packages to version 11.1.2 in package-lock.json.
- Add dompurify as a dependency for monaco-editor in package.json.
- Update d3-format to version 3.1.2 and dagre-d3-es to version 7.0.14 in package-lock.json.
- Upgrade dompurify to version 3.3.2 in package-lock.json.
* chore: update language prop in ArtifactCodeEditor for read-only mode for better UX
- Adjusted the language prop in the MonacoEditor component to use 'plaintext' when in read-only mode, ensuring proper display of content without syntax highlighting.
* fix: include remoteAgents config in loadDefaultInterface
The loadDefaultInterface function was not passing the remoteAgents
configuration from librechat.yaml to the permission system, causing
remoteAgents permissions to never update from the YAML config even
when explicitly configured.
This fix adds the missing remoteAgents field to the returned
loadedInterface object, allowing the permission update system to
properly detect and apply remoteAgents configuration from the YAML file.
Fixes remote agents (API) configuration not being applied from librechat.yaml
* test: Add remoteAgents permission tests for USER and ADMIN roles
Introduced new tests to validate the application of remoteAgents configuration in user permissions. The tests cover scenarios for explicit configuration, full enablement, and default role behavior when remoteAgents are not configured. This ensures that permissions are correctly applied based on the provided configuration, addressing a regression related to the omission of remoteAgents in the loadDefaultInterface function.
---------
Co-authored-by: Airam Hernández Hernández <airam.hernandez@intelequia.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* feat: MCP server reinitialization recovery mechanism
- Added functionality to store a stub configuration for MCP servers that fail inspection at startup, allowing for recovery via reinitialization.
- Introduced `reinspectServer` method in `MCPServersRegistry` to handle reinspection of previously failed servers.
- Enhanced `MCPServersInitializer` to log and manage server initialization failures, ensuring proper handling of inspection failures.
- Added integration tests to verify the recovery process for unreachable MCP servers, ensuring that stub configurations are stored and can be reinitialized successfully.
- Updated type definitions to include `inspectionFailed` flag in server configurations for better state management.
* fix: MCP server handling for inspection failures
- Updated `reinitMCPServer` to return a structured response when the server is unreachable, providing clearer feedback on the failure.
- Modified `ConnectionsRepository` to prevent connections to servers marked as inspection failed, improving error handling.
- Adjusted `MCPServersRegistry` methods to ensure proper management of server states, including throwing errors for non-failed servers during reinspection.
- Enhanced integration tests to validate the behavior of the system when dealing with unreachable MCP servers and inspection failures, ensuring robust recovery mechanisms.
* fix: Clear all cached server configurations in MCPServersRegistry
- Added a comment to clarify the necessity of clearing all cached server configurations when updating a server's configuration, as the cache is keyed by userId without a reverse index for enumeration.
* fix: Update integration test for file_tools_server inspection handling
- Modified the test to verify that the `file_tools_server` is stored as a stub when inspection fails, ensuring it can be reinitialized correctly.
- Adjusted expectations to confirm that the `inspectionFailed` flag is set to true for the stub configuration, enhancing the robustness of the recovery mechanism.
* test: Add unit tests for reinspecting servers in MCPServersRegistry
- Introduced tests for the `reinspectServer` method to validate error handling when called on a healthy server and when the server does not exist.
- Ensured that appropriate exceptions are thrown for both scenarios, enhancing the robustness of server state management.
* test: Add integration test for concurrent reinspectServer calls
- Introduced a new test to validate that multiple concurrent calls to reinspectServer do not crash or corrupt the server state.
- Ensured that at least one call succeeds and any failures are due to the server not being in a failed state, enhancing the reliability of the reinitialization process.
* test: Enhance integration test for concurrent MCP server reinitialization
- Added a new test to validate that concurrent calls to reinitialize the MCP server do not crash or corrupt the server state.
- Ensured that at least one call succeeds and that failures are handled gracefully, improving the reliability of the reinitialization process.
- Reset MCPManager instance after each test to maintain a clean state for subsequent tests.
* fix: remove docker images by named tag instead of image ID
* refactor: Simplify rebase logic and enhance error handling in deployed-update script
- Removed unnecessary condition for rebasing, streamlining the update process.
- Renamed variable for clarity when fetching Docker image tags.
- Added error handling to catch and log failures during the update process, ensuring better visibility of issues.
* 🔒 fix: handle hex-normalized IPv4-mapped IPv6 in domain validation
* fix: Enhance IPv6 private address detection in domain validation
- Added tests for detecting IPv4-compatible, 6to4, NAT64, and Teredo addresses.
- Implemented `extractEmbeddedIPv4` function to identify private IPv4 addresses within various IPv6 formats.
- Updated `isPrivateIP` function to utilize the new extraction logic for improved accuracy in address validation.
* fix: Update private IPv4 detection logic in domain validation
- Enhanced the `isPrivateIPv4` function to accurately identify additional private and non-routable IPv4 ranges.
- Adjusted the return logic in `resolveHostnameSSRF` to utilize the updated private IP detection for improved hostname validation.
* test: Expand private IP detection tests in domain validation
- Added tests for additional private IPv4 ranges including 0.0.0.0/8, 100.64.0.0/10, 192.0.0.0/24, and 198.18.0.0/15.
- Updated existing tests to ensure accurate detection of private and multicast IP addresses in the `isPrivateIP` function.
- Enhanced `resolveHostnameSSRF` to correctly identify private literal IPv4 addresses without DNS lookup.
* refactor: Rename and enhance embedded IPv4 detection in IPv6 addresses
- Renamed `extractEmbeddedIPv4` to `hasPrivateEmbeddedIPv4` for clarity on its purpose.
- Updated logic to accurately check for private IPv4 addresses embedded in Teredo, 6to4, and NAT64 IPv6 formats.
- Improved the `isPrivateIP` function to utilize the new naming and logic for better readability and accuracy.
- Enhanced documentation for clarity on the functionality of the updated methods.
* feat: Enhance private IPv4 detection in embedded IPv6 addresses
- Added additional checks in `hasPrivateEmbeddedIPv4` to ensure only valid private IPv4 formats are recognized.
- Improved the logic for identifying private IPv4 addresses embedded within various IPv6 formats, enhancing overall accuracy.
* test: Add additional test for hostname resolution in SSRF detection
- Included a new test case in `resolveHostnameSSRF` to validate the detection of private IPv4 addresses embedded in IPv6 formats for the hostname 'meta.example.com'.
- Enhanced existing tests to ensure comprehensive coverage of hostname resolution scenarios.
* fix: Set redirect option to 'manual' in undiciFetch calls
- Updated undiciFetch calls in MCPConnection to include the redirect option set to 'manual' for better control over HTTP redirects.
- Added documentation comments regarding SSRF pre-checks for WebSocket connections, highlighting the limitations of the current SDK regarding DNS resolution.
* test: Add integration tests for MCP SSRF protections
- Introduced a new test suite for MCP SSRF protections, verifying that MCPConnection does not follow HTTP redirects to private IPs and blocks WebSocket connections to private IPs when SSRF protection is enabled.
- Implemented tests to ensure correct behavior of the connection under various scenarios, including redirect handling and WebSocket DNS resolution.
* refactor: Improve SSRF protection logic for WebSocket connections
- Enhanced the SSRF pre-check for WebSocket connections to validate resolved IPs, ensuring that allowlisting a domain does not grant trust to its resolved IPs at runtime.
- Updated documentation comments to clarify the limitations of the current SDK regarding DNS resolution and the implications for SSRF protection.
* test: Enhance MCP SSRF protection tests for redirect handling and WebSocket connections
- Updated tests to ensure that MCPConnection does not follow HTTP redirects to private IPs, regardless of SSRF protection settings.
- Added checks to verify that WebSocket connections to hosts resolving to private IPs are blocked, even when SSRF protection is disabled.
- Improved documentation comments for clarity on the behavior of the tests and the implications for SSRF protection.
* test: Refactor MCP SSRF protection test for WebSocket connection errors
- Updated the test to use `await expect(...).rejects.not.toThrow(...)` for better readability and clarity.
- Simplified the error handling logic while ensuring that SSRF rejections are correctly validated during connection failures.
* chore: Remove unused setValueOnChange prop from MCPServerMenuItem component
* fix: Resolve agent provider endpoint type for file upload support
When using the agents endpoint with a custom provider (e.g., Moonshot),
the endpointType was resolving to "agents" instead of the provider's
actual type ("custom"), causing "Upload to Provider" to not appear in
the file attach menu.
Adds `resolveEndpointType` utility in data-provider that follows the
chain: endpoint (if not agents) → agent.provider → agents. Applied
consistently across AttachFileChat, DragDropContext, useDragHelpers,
and AgentPanel file components (FileContext, FileSearch, Code/Files).
* refactor: Extract useAgentFileConfig hook, restore deleted tests, fix review findings
- Extract shared provider resolution logic into useAgentFileConfig hook
(Finding #2: DRY violation across FileContext, FileSearch, Code/Files)
- Restore 18 deleted test cases in AttachFileMenu.spec.tsx covering
agent capabilities, SharePoint, edge cases, and button state
(Finding #1: accidental test deletion)
- Wrap fileConfigEndpoint in useMemo in AttachFileChat (Finding #3)
- Fix misleading test name in AgentFileConfig.spec.tsx (Finding #4)
- Fix import order in FileSearch.tsx, FileContext.tsx, Code/Files.tsx (Finding #5)
- Add comment about cache gap in useDragHelpers (Finding #6)
- Clarify resolveEndpointType JSDoc (Finding #7)
* refactor: Memoize Footer component for performance optimization
- Converted Footer component to a memoized version to prevent unnecessary re-renders.
- Improved import structure by adding memo to the React import statement for clarity.
* chore: Fix remaining review nits
- Widen useAgentFileConfig return type to EModelEndpoint | string
- Fix import order in FileContext.tsx and FileSearch.tsx
- Remove dead endpointType param from setupMocks in AttachFileMenu test
* fix: Pass resolved provider endpoint to file upload validation
AgentPanel file components (FileContext, FileSearch, Code/Files) were
hardcoding endpointOverride to "agents", causing both client-side
validation (file limits, MIME types) and server-side validation to
use the agents config instead of the provider-specific config.
Adds endpointTypeOverride to UseFileHandling params so endpoint and
endpointType can be set independently. Components now pass the
resolved provider name and type from useAgentFileConfig, so the full
fallback chain (provider → custom → agents → default) applies to
file upload validation on both client and server.
* test: Verify any custom endpoint is document-supported regardless of name
Adds parameterized tests with arbitrary endpoint names (spaces, hyphens,
colons, etc.) confirming that all custom endpoints resolve to
document-supported through resolveEndpointType, both as direct
endpoints and as agent providers.
* fix: Use || for provider fallback, test endpointOverride wiring
- Change providerValue ?? to providerValue || so empty string is
treated as "no provider" consistently with resolveEndpointType
- Add wiring tests to CodeFiles, FileContext, FileSearch verifying
endpointOverride and endpointTypeOverride are passed correctly
- Update endpointOverride JSDoc to document endpointType fallback
* refactor: Update Image Component to Remove Lazy Loading and Enhance Rendering
- Removed the react-lazy-load-image-component dependency from the Image component, simplifying the image loading process.
- Updated the Image component to use a standard <img> tag with async decoding for improved performance and user experience.
- Adjusted related tests to reflect changes in image rendering behavior and ensure proper functionality without lazy loading.
* refactor: Enhance Image Handling and Caching Across Components
- Introduced a new previewCache utility for managing local blob preview URLs, improving image loading efficiency.
- Updated the Image component and related parts (FileRow, Files, Part, ImageAttachment, LogContent) to utilize cached previews, enhancing rendering performance and user experience.
- Added width and height properties to the Image component for better layout management and consistency across different usages.
- Improved file handling logic in useFileHandling to cache previews during file uploads, ensuring quick access to image data.
- Enhanced overall code clarity and maintainability by streamlining image rendering logic and reducing redundancy.
* refactor: Enhance OpenAIImageGen Component with Image Dimensions
- Added width and height properties to the OpenAIImageGen component for improved image rendering and layout management.
- Updated the Image component usage within OpenAIImageGen to utilize the new dimensions, enhancing visual consistency and performance.
- Improved code clarity by destructuring additional properties from the attachment object, streamlining the component's logic.
* refactor: Implement Image Size Caching in DialogImage Component
- Introduced an imageSizeCache to store and retrieve image sizes, enhancing performance by reducing redundant fetch requests.
- Updated the getImageSize function to first check the cache before making network requests, improving efficiency in image handling.
- Added decoding attribute to the image element for optimized rendering behavior.
* refactor: Enhance UserAvatar Component with Avatar Caching and Error Handling
- Introduced avatar caching logic to optimize avatar resolution based on user ID and avatar source, improving performance and reducing redundant image loads.
- Implemented error handling for failed image loads, allowing for fallback to a default avatar when necessary.
- Updated UserAvatar props to streamline the interface by removing the user object and directly accepting avatar-related properties.
- Enhanced overall code clarity and maintainability by refactoring the component structure and logic.
* fix: Layout Shift in Message and Placeholder Components for Consistent Height Management
- Adjusted the height of the PlaceholderRow and related message components to ensure consistent rendering with a minimum height of 31px.
- Updated the MessageParts and ContentRender components to utilize a minimum height for better layout stability.
- Enhanced overall code clarity by standardizing the structure of message-related components.
* tests: Update FileRow Component to Prefer Cached Previews for Image Rendering
- Modified the image URL selection logic in the FileRow component to prioritize cached previews over file paths when uploads are complete, enhancing rendering performance and user experience.
- Updated related tests to reflect changes in image URL handling, ensuring accurate assertions for both preview and file path scenarios.
- Introduced a fallback mechanism to use file paths when no preview exists, improving robustness in file handling.
* fix: Image cache lifecycle and dialog decoding
- Add deletePreview/clearPreviewCache to previewCache.ts for blob URL cleanup
- Wire deletePreview into useFileDeletion to revoke blobs on file delete
- Move dimensionCache.set into useMemo to avoid side effects during render
- Extract IMAGE_MAX_W_PX constant (512) to document coupling with max-w-lg
- Export _resetImageCaches for test isolation
- Change DialogImage decoding from "sync" to "async" to avoid blocking main thread
* fix: Avatar cache invalidation and cleanup
- Include avatarSrc in cache invalidation to prevent stale avatars
- Remove unused username parameter from resolveAvatar
- Skip caching when userId is empty to prevent cache key collisions
* test: Fix test isolation and type safety
- Reset module-level dimensionCache/paintedUrls in beforeEach via _resetImageCaches
- Replace any[] with typed mock signature in cn mock for both test files
* chore: Code quality improvements from review
- Use barrel imports for previewCache in Files.tsx and Part.tsx
- Single Map.get with truthy check instead of has+get in useEventHandlers
- Add JSDoc comments explaining EmptyText margin removal and PlaceholderRow height
- Fix FileRow toast showing "Deleting file" when file isn't actually deleted (progress < 1)
* fix: Address remaining review findings (R1-R3)
- Add deletePreview calls to deleteFiles batch path to prevent blob URL leaks
- Change useFileDeletion import from deep path to barrel (~/utils)
- Change useMemo to useEffect for dimensionCache.set (side effect, not derived value)
* fix: Address audit comments 2, 5, and 7
- Fix files preservation to distinguish null (missing) from [] (empty) in finalHandler
- Add auto-revoke on overwrite in cachePreview to prevent leaked blobs
- Add removePreviewEntry for key transfer without revoke
- Clean up stale temp_file_id cache entry after promotion to permanent file_id
* fix: default ALLOW_SHARED_LINKS_PUBLIC to false for security
Shared links were publicly accessible by default when
ALLOW_SHARED_LINKS_PUBLIC was not explicitly set, which could lead to
unintentional data exposure. Users may assume their authentication
settings protect shared links when they do not.
This changes the default behavior so shared links require JWT
authentication unless ALLOW_SHARED_LINKS_PUBLIC is explicitly set to
true.
* Document ALLOW_SHARED_LINKS_PUBLIC in .env.example
Add comment explaining ALLOW_SHARED_LINKS_PUBLIC setting.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Danny Avila <danacordially@gmail.com>
- Add useHasAccess hook for TEMPORARY_CHAT permission type
- Conditionally render TemporaryChat component based on user permissions
- Ensures feature respects role-based access control
Co-authored-by: Airam Hernández Hernández <airam.hernandez@intelequia.com>
* fix: remove scaleImage function that stretched vertical images
* chore: lint
* refactor: Simplify Image Component Usage Across Chat Parts
- Removed height and width props from the Image component in various parts (Files, Part, ImageAttachment, LogContent) to streamline image rendering.
- Introduced a constant for maximum image height in the Image component for consistent styling.
- Updated related components to utilize the new simplified Image component structure, enhancing maintainability and reducing redundancy.
* refactor: Simplify LogContent and Enhance Image Component Tests
- Removed height and width properties from the ImageAttachment type in LogContent for cleaner code.
- Updated the image rendering logic to rely solely on the filepath, improving clarity.
- Enhanced the Image component tests with additional assertions for rendering behavior and accessibility.
- Introduced new tests for OpenAIImageGen to validate image preloading and progress handling, ensuring robust functionality.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* refactor: Code Editor and Auto Scroll Functionality
- Added a useEffect hook in CodeEditor to sync streaming content with Sandpack without remounting the provider, improving performance and user experience.
- Updated useAutoScroll to accept an optional editorRef, allowing for dynamic scroll container selection based on the editor's state.
- Refactored ArtifactTabs to utilize the new editorRef in the useAutoScroll hook, ensuring consistent scrolling behavior during content updates.
- Introduced stableFiles and mergedFiles logic in CodeEditor to optimize file handling and prevent unnecessary updates during streaming content changes.
* refactor: Update CodeEditor to Sync Streaming Content Based on Read-Only State
- Modified the useEffect hook in CodeEditor to conditionally sync streaming content with Sandpack only when in read-only mode, preventing unnecessary updates during user edits.
- Enhanced the dependency array of the useEffect hook to include the readOnly state, ensuring accurate synchronization behavior.
* refactor: Monaco Editor for Artifact Code Editing
* refactor: Clean up ArtifactCodeEditor and ArtifactTabs components
- Removed unused scrollbar styles from mobile.css to streamline the code.
- Refactored ArtifactCodeEditor to improve content synchronization and read-only state handling.
- Enhanced ArtifactTabs by removing unnecessary context usage and optimizing component structure for better readability.
* feat: Add support for new artifact type 'application/vnd.ant.react'
- Introduced handling for 'application/vnd.ant.react' in artifactFilename, artifactTemplate, and dependenciesMap.
- Updated relevant mappings to ensure proper integration of the new artifact type within the application.
* refactor:ArtifactCodeEditor with Monaco Editor Configuration
- Added support for disabling validation in the Monaco Editor to improve the artifact viewer/editor experience.
- Introduced a new type definition for Monaco to enhance type safety.
- Updated the handling of the 'application/vnd.ant.react' artifact type to ensure proper integration with the editor.
* refactor: Clean up ArtifactCodeEditor and mobile.css
- Removed unnecessary whitespace in mobile.css for cleaner code.
- Refactored ArtifactCodeEditor to streamline language mapping and type handling, enhancing readability and maintainability.
- Consolidated language and type mappings into dedicated constants for improved clarity and efficiency.
* feat: Integrate Monaco Editor for Enhanced Code Editing Experience
- Added the Monaco Editor as a dependency to improve the code editing capabilities within the ArtifactCodeEditor component.
- Refactored the handling of TypeScript and JavaScript defaults in the Monaco Editor configuration for better type safety and clarity.
- Streamlined the setup for disabling validation, enhancing the artifact viewer/editor experience.
* fix: Update ArtifactCodeEditor to handle null content checks
- Modified conditional checks in ArtifactCodeEditor to use `art.content != null` instead of `art.content` for improved null safety.
- Ensured consistent handling of artifact content across various useEffect hooks to prevent potential errors when content is null.
* fix: Refine content comparison logic in ArtifactCodeEditor
- Updated the condition for checking if the code is not original by removing the redundant null check for `art.content`, ensuring more concise and clear logic.
- This change enhances the readability of the code and maintains the integrity of content comparison within the editor.
* fix: Simplify code comparison logic in ArtifactCodeEditor
- Removed redundant null check for the `code` variable, ensuring a more straightforward comparison with the current update reference.
- This change improves code clarity and maintains the integrity of the content comparison logic within the editor.
* ✨ feat: Add support for new GPT-5.4 and GPT-5.4-pro models
- Introduced new token values and cache settings for 'gpt-5.4' and 'gpt-5.4-pro' in the API model configurations.
- Updated maximum output limits for the new models in the tokens utility.
- Included 'gpt-5.4' and 'gpt-5.4-pro' in the shared OpenAI models list for consistent access across the application.
* 🔧 update: Enhance GPT-5.4 and GPT-5.4-pro model configurations
- Refined token pricing and cache settings for 'gpt-5.4' and 'gpt-5.4-pro' in the API model configurations.
- Added tests for cache multipliers and maximum token limits for the new models.
- Updated shared OpenAI models list to include 'gpt-5.4-thinking' and added a note for verifying pricing before release.
* 🔧 update: Add clarification to token pricing for 'gpt-5.4-pro'
- Added a comment to the 'gpt-5.4-pro' model configuration in tokens.ts to specify that it shares the same token window as 'gpt-5.4', enhancing clarity for future reference.
* 🔧 fix: Update Excel sheet parsing to use fs.promises.readFile and correct import for xlsx
- Modified the excelSheetToText function to read the file using fs.promises.readFile instead of directly accessing the file path.
- Updated the import statement for the xlsx library to use the correct read method, ensuring proper functionality in parsing Excel sheets.
* 🔧 fix: Update document parsing methods to use buffer for file reading
- Modified the wordDocToText function to read the file as a buffer using fs.promises.readFile, ensuring compatibility with the mammoth library.
- Updated the excelSheetToText function to read the Excel file as a buffer, addressing issues with the xlsx library's handling of dynamic imports and file access.
* feat: Add tests for empty xlsx document parsing and validate xlsx imports
- Introduced a new test case to verify that the `parseDocument` function correctly handles an empty xlsx file with only a sheet name, ensuring it returns the expected document structure.
- Added a test to confirm that the `xlsx` library exports `read` and `utils` as named imports, validating the functionality of the library integration.
- Included a new empty xlsx file to support the test cases.
* 🔄 refactor: Update Artifacts and Messages Contexts to Use Latest Message ID and Depth
- Modified ArtifactsContext to retrieve latestMessage using Recoil state management.
- Updated MessagesViewContext to replace latestMessage with latestMessageId and latestMessageDepth for improved clarity and consistency.
- Adjusted various components (HoverButtons, MessageParts, MessageRender, ContentRender) to utilize latestMessageId instead of the entire message object, enhancing performance and reducing unnecessary re-renders.
- Refactored useChatHelpers to extract latestMessageId and latestMessageDepth, streamlining message handling across the application.
* refactor: Introduce PartWithContext Component for Optimized Message Rendering
- Added a new PartWithContext component to encapsulate message part rendering logic, improving context management and reducing redundancy in the ContentParts component.
- Updated MessageRender to utilize the new PartWithContext, streamlining the context provider setup and enhancing code clarity.
- Refactored related logic to ensure proper context values are passed, improving maintainability and performance in message rendering.
* refactor: Update Components to Use Function Declarations and Improve Readability
- Refactored several components (MessageContainer, Markdown, MarkdownCode, MarkdownCodeNoExecution, MarkdownAnchor, MarkdownParagraph, MarkdownImage, TextPart, PlaceholderRow) to use function declarations instead of arrow functions, enhancing readability and consistency across the codebase.
- Added display names to memoized components for better debugging and profiling in React DevTools.
- Improved overall code clarity and maintainability by standardizing component definitions.
* refactor: Standardize MessageRender and ContentRender Components for Improved Clarity
- Refactored MessageRender and ContentRender components to use function declarations, enhancing readability and consistency.
- Streamlined props handling by removing unnecessary parameters and improving the use of hooks for state management.
- Updated memoization and rendering logic to optimize performance and reduce unnecessary re-renders.
- Enhanced overall code clarity and maintainability by standardizing component definitions and structure.
* refactor: Enhance Header Component with Memoization for Performance
- Refactored the Header component to utilize React's memoization by wrapping it with the memo function, improving rendering performance by preventing unnecessary re-renders.
- Changed the export to a memoized version of the Header component, ensuring better debugging with a display name.
- Maintained overall code clarity and consistency in component structure.
* refactor: Transition Components to Use Recoil for State Management
- Updated multiple components (AddMultiConvo, TemporaryChat, HeaderNewChat, PresetsMenu, ModelSelectorChatContext) to utilize Recoil for state management, enhancing consistency and performance.
- Replaced useChatContext with Recoil selectors and atoms, improving data flow and reducing unnecessary re-renders.
- Introduced new selectors for conversation ID and endpoint retrieval, streamlining component logic and enhancing maintainability.
- Improved overall code clarity by standardizing state management practices across components.
* refactor: Integrate getConversation Callback for Enhanced State Management
- Updated multiple components (Mention, ModelSelectorChatContext, ModelSelectorContext, FavoritesList) to utilize a getConversation callback instead of directly accessing conversation state, improving encapsulation and maintainability.
- Refactored useSelectMention hook to accept getConversation, streamlining conversation retrieval and enhancing code clarity.
- Introduced new Recoil selectors for conversation properties, ensuring consistent state management across components.
- Enhanced overall code structure by standardizing the approach to conversation handling, reducing redundancy and improving performance.
* refactor: Optimize LiveAnnouncer Context Value with useMemo
- Updated the LiveAnnouncer component to utilize useMemo for context value creation, enhancing performance by preventing unnecessary recalculations of the context object.
- Improved overall code clarity and maintainability by ensuring that context values are only recomputed when their dependencies change.
* refactor: Update AgentPanelSwitch to Use Recoil for Agent ID Management
- Refactored AgentPanelSwitch component to utilize Recoil for retrieving the current agent ID, replacing the previous use of chat context.
- Improved state management by ensuring the agent ID is derived from Recoil, enhancing code clarity and maintainability.
- Adjusted useEffect dependencies to reflect the new state management approach, streamlining the component's logic.
* refactor: Enhance useLocalize Hook with useCallback for Improved Performance
- Updated the useLocalize hook to utilize useCallback for the translation function, optimizing performance by preventing unnecessary re-creations of the function on each render.
- Improved code clarity by ensuring that the translation function is memoized, enhancing maintainability and efficiency in localization handling.
* refactor: Rename useCreateConversationAtom to useSetConversationAtom for Clarity
- Updated the hook name from useCreateConversationAtom to useSetConversationAtom to better reflect its functionality in managing conversation state.
- Introduced a new implementation for setting conversation state, enhancing clarity and maintainability in the codebase.
- Adjusted related references in the useNewConvo hook to align with the new naming convention.
* refactor: Enhance useKeyDialog Hook with useMemo and useCallback for Improved Performance
- Updated the useKeyDialog hook to utilize useMemo for returning the dialog state and handlers, optimizing performance by preventing unnecessary recalculations.
- Refactored the onOpenChange function to use useCallback, ensuring it only changes when its dependencies do, enhancing maintainability and clarity in the code.
- Improved overall code structure and readability by streamlining the hook's logic and dependencies.
* feat: Add useRenderChangeLog Hook for Debugging Render Changes
- Introduced a new hook, useRenderChangeLog, that logs changes in tracked values between renders when a debug flag is enabled.
- Utilizes useEffect and useRef to track previous values and identify changes, enhancing debugging capabilities for component renders.
- Provides detailed console output for initial renders and value changes, improving developer insights during the rendering process.
* refactor: Update useSelectAgent Hook for Improved State Management and Performance
- Refactored the useSelectAgent hook to utilize useRecoilCallback for fetching conversation data, enhancing state management and performance.
- Replaced the use of useChatContext with a more efficient approach, streamlining the logic for selecting agents and updating conversations.
- Improved error handling and ensured asynchronous operations are properly awaited, enhancing reliability in agent selection and data fetching processes.
* refactor: Optimize useDefaultConvo Hook with useCallback for Improved Performance
- Refactored the getDefaultConversation function within the useDefaultConvo hook to utilize useCallback, enhancing performance by memoizing the function and preventing unnecessary re-creations on re-renders.
- Streamlined the logic for cleaning input and output in the conversation object, improving code clarity and maintainability.
- Ensured that dependencies for useCallback are correctly set, enhancing the reliability of the hook's behavior.
* refactor: Optimize Agent Components with Memoization for Improved Performance
- Refactored multiple agent-related components (AgentAvatar, AgentCategorySelector, AgentSelect, DeleteButton, FileContext, FileSearch, Files) to utilize React.memo for memoization, enhancing rendering performance by preventing unnecessary re-renders.
- Updated the FileRow component to make setFilesLoading optional, improving flexibility in file handling.
- Streamlined component logic and improved maintainability by ensuring that props are compared efficiently in memoized components.
* refactor: Enhance File Handling and Agent Components for Improved Performance
- Refactored multiple components (DeleteButton, FileContext, FileSearch, Files) to utilize new file handling hooks that separate chat context from file operations, improving performance and maintainability.
- Introduced useFileHandlingNoChatContext and useSharePointFileHandlingNoChatContext hooks to streamline file handling logic, enhancing flexibility in managing file states.
- Updated DeleteButton to improve conversation state management and ensure proper handling of agent deletions, enhancing user experience.
- Optimized imports and component structure for better clarity and organization across the affected files.
* refactor: Enhance useRenderChangeLog Hook with Improved Type Safety and Documentation
- Updated the useRenderChangeLog hook to improve type safety by specifying the value types as string, number, boolean, null, or undefined.
- Enhanced documentation to clarify usage and enablement of the debug feature, ensuring better developer insights during rendering.
- Added a production check to prevent logging in production builds, optimizing performance and maintaining clean console output.
* chore: imports
* refactor: Replace useRecoilCallback with useGetConversation Hook for Improved Clarity and Performance
- Refactored multiple components (AddMultiConvo, ModelSelectorChatContext, FavoritesList, useSelectAgent, usePresets) to utilize the new useGetConversation hook, enhancing clarity and reducing complexity by eliminating the use of useRecoilCallback.
- Streamlined conversation retrieval logic across components, improving maintainability and performance.
- Updated imports and component structure for better organization and readability.
* refactor: Enhance Memoization in DeleteButton Component for Improved Performance
- Updated the memoization logic in the DeleteButton component to include a comparison for the setCurrentAgentId prop, ensuring more efficient re-renders.
- This change improves performance by preventing unnecessary updates when the agent ID and current agent ID remain unchanged.
* chore: fix test
* refactor: Improve Memoization Logic in AgentSelect Component
- Updated the memoization comparison in the AgentSelect component to directly compare agentQuery.data objects, enhancing performance by ensuring accurate re-renders.
- Refactored the useCreateConversationAtom function to streamline the logic for updating conversation keys, improving clarity and maintainability.
* refactor: Simplify State Management in DeleteButton Component
- Removed unnecessary setConversationOption function, streamlining the logic for updating conversation state after agent deletion.
- Updated the conversation state directly within the deleteAgent mutation, improving clarity and maintainability of the component.
- Refactored conversationByKeySelector to directly reference conversationByIndex, enhancing performance and reducing complexity in state retrieval.
* refactor: Remove Unused Conversation Prop from Mention Component
- Eliminated the conversation prop from the Mention component, simplifying its interface and reducing unnecessary dependencies.
- Updated the ChatForm component to reflect this change, enhancing clarity and maintainability of the codebase.
- Introduced useGetConversation hook for improved conversation retrieval logic, streamlining the component's functionality.
* refactor: Simplify File Handling State Management Across Components
- Removed the unused setFilesLoading function from FileContext, FileSearch, and Files components, streamlining the file handling state management.
- Updated the FileHandlingState type to make setFilesLoading optional, enhancing flexibility in file operations.
- Improved memoization logic by directly referencing necessary state properties, ensuring better performance and maintainability.
* refactor: Update ArtifactsContext for Improved State Management
- Replaced the useChatContext hook with direct Recoil state retrieval for isSubmitting, latestMessage, and conversationId, simplifying the context provider's logic.
- Enhanced memoization by ensuring relevant state properties are directly referenced, improving performance and maintainability.
- Streamlined the context value creation to reflect the updated state management approach.
* refactor: Adjust Memoization Logic in ArtifactsContext for Consistency
- Updated the memoization logic in the ArtifactsProvider to ensure the messageId is consistently referenced, improving clarity and maintainability.
- This change enhances the performance of the context provider by ensuring all relevant properties are included in the memoization dependencies.
* refactor: CI Workflow for Backend with Build and Test Jobs
- Updated the GitHub Actions workflow to include a new build job that compiles packages and uploads build artifacts.
- Added separate test jobs for each package (`api`, `data-provider`, and `data-schemas`) to run unit tests after the build process.
- Introduced caching for build artifacts to optimize build times.
- Configured Jest to utilize 50% of available workers for improved test performance across all Jest configurations in the `api`, `data-schemas`, and `packages/api` directories.
* refactor: Update CI Workflow for Backend with Enhanced Build and Cache Management
- Modified the GitHub Actions workflow to improve the build process by separating build and cache steps for `data-provider`, `data-schemas`, and `api` packages.
- Updated artifact upload and download steps to reflect the new naming conventions for better clarity.
- Enhanced caching strategies to optimize build times and ensure efficient artifact management.
* chore: Node Modules Caching in CI Workflow
- Updated the GitHub Actions workflow to implement caching for the `node_modules` directory, improving build efficiency by restoring cached dependencies.
- Adjusted the installation step to conditionally run based on cache availability, optimizing the overall CI process.
* refactor: Enhance CI Workflow for Frontend with Build and Test Jobs
- Updated the GitHub Actions workflow to introduce a structured build process for frontend packages, including separate jobs for building and testing on both Ubuntu and Windows environments.
- Implemented caching strategies for `node_modules` and build artifacts to optimize build times and improve efficiency.
- Added artifact upload and download steps for `data-provider` and `client-package` builds, ensuring that builds are reused across jobs.
- Adjusted Node.js version specification for consistency and reliability across different jobs.
* refactor: Update CI Workflows for Backend and Frontend with Node.js 20.19 and Enhanced Caching
- Updated Node.js version to 20.19 across all jobs in both backend and frontend workflows for consistency.
- Enhanced caching strategies for build artifacts and `node_modules`, increasing retention days from 1 to 2 for better efficiency.
- Adjusted cache keys to include additional files for improved cache hit rates during builds.
- Added conditional installation of dependencies to optimize the CI process.
* chore: Configure Jest to Use 50% of Available Workers Across Client and Data Provider
- Added `maxWorkers: '50%'` setting to Jest configuration files for the client and data provider packages to optimize test performance by utilizing half of the available CPU cores during test execution.
* chore: Enhance Node Modules Caching in CI Workflows
- Updated caching paths in both backend and frontend GitHub Actions workflows to include additional `node_modules` directories for improved dependency management.
- This change optimizes the caching strategy, ensuring that all relevant modules are cached, which can lead to faster build times and more efficient CI processes.
* chore: Update Node Modules Cache Keys in CI Workflows
- Modified cache keys in both backend and frontend GitHub Actions workflows to include the Node.js version (20.19) for improved cache management.
- This change ensures that the caching mechanism is more specific, potentially enhancing cache hit rates and build efficiency.
* chore: Refactor Node Modules Cache Keys in CI Workflows
- Updated cache keys in backend and frontend GitHub Actions workflows to be more specific, distinguishing between frontend and backend caches.
- Removed references to `client/node_modules` in backend workflows to streamline caching paths and improve cache management.
* refactor: Add timestamps option to updateMany in createMeiliMongooseModel
- Updated the updateMany call in createMeiliMongooseModel to include a timestamps option set to false, ensuring that the operation does not modify the document's timestamps during the indexing process. This change improves the accuracy of document state management in MongoDB.
* test: Add tests to ensure updatedAt timestamps are preserved during syncWithMeili
- Introduced new test cases for the processSyncBatch function to verify that the original updatedAt timestamps on conversations and messages remain unchanged after synchronization with Meilisearch. This enhancement ensures data integrity during the indexing process.
* docs: Update comments in createMeiliMongooseModel to clarify timestamp preservation
- Enhanced comments in the createMeiliMongooseModel function to explain the use of the { timestamps: false } option in the updateMany call, ensuring that original conversation/message timestamps are preserved during the indexing process. This change improves code clarity and maintains the integrity of document timestamps.
* test: Enhance Meilisearch sync tests to verify updatedAt timestamp preservation
- Added assertions to ensure that the updatedAt timestamps of documents remain unchanged before and after synchronization with Meilisearch. This update improves the test coverage for the syncWithMeili function, reinforcing data integrity during the indexing process.
* fix: subdirectory redirects
* fix: use path-segment boundary check when stripping BASE_URL prefix
A bare `startsWith(BASE_URL)` matches on character prefix, not path
segments. With BASE_URL="/chat", a path like "/chatroom/c/abc" would
incorrectly strip to "room/c/abc" (no leading slash). Guard with an
exact-match-or-slash check: `p === BASE_URL || p.startsWith(BASE_URL + '/')`.
Also removes the dead `BASE_URL !== '/'` guard — module init already
converts '/' to ''.
* test: add path-segment boundary tests and clarify subdirectory coverage
- Add /chatroom, /chatbot, /app/chatroom regression tests to verify
BASE_URL stripping only matches on segment boundaries
- Clarify useAuthRedirect subdirectory test documents React Router
basename behavior (BASE_URL stripping tested in api-endpoints-subdir)
- Use `delete proc.browser` instead of undefined assignment for cleanup
- Add rationale to eslint-disable comment for isolateModules require
* fix: use relative path and correct instructions in subdirectory test script
- Replace hardcoded /home/danny/LibreChat/.env with repo-root-relative
path so the script works from any checkout location
- Update instructions to use production build (npm run build && npm run
backend) since nginx proxies to :3080 which only serves the SPA after
a full build, not during frontend:dev on :3090
* fix: skip pointless redirect_to=/ for root path and fix jsdom 26+ compat
buildLoginRedirectUrl now returns plain /login when the resolved path
is root — redirect_to=/ adds no value since / immediately redirects
to /c/new after login anyway.
Also rewrites api-endpoints.spec.ts to use window.history.replaceState
instead of Object.defineProperty(window, 'location', ...) which jsdom
26+ no longer allows.
* test: fix request-interceptor.spec.ts for jsdom 26+ compatibility
Switch from jsdom to happy-dom environment which allows
Object.defineProperty on window.location. jsdom 26+ made
location non-configurable, breaking all 8 tests in this file.
* chore: update browser property handling in api-endpoints-subdir test
Changed the handling of the `proc.browser` property from deletion to setting it to false, ensuring compatibility with the current testing environment.
* chore: update backend restart instructions in test subdirectory setup script
Changed the instruction for restarting the backend from "npm run backend:dev" to "npm run backend" to reflect the correct command for the current setup.
* refactor: ensure proper cleanup in loadModuleWithBase function
Wrapped the module loading logic in a try-finally block to guarantee that the `proc.browser` property is reset to false and the base element is removed, improving reliability in the testing environment.
* refactor: improve browser property handling in loadModuleWithBase function
Revised the management of the `proc.browser` property to store the original value before modification, ensuring it is restored correctly after module loading. This enhances the reliability of the testing environment.
* chore: npm audit
- Bumped versions for several packages: `@hono/node-server` to 1.19.10, `@tootallnate/once` to 3.0.1, `hono` to 4.12.5, `serialize-javascript` to 7.0.4, and `svgo` to 2.8.2.
- Removed deprecated `@trysound/sax` package from package-lock.json.
- Updated integrity hashes and resolved URLs in package-lock.json to reflect the new versions.
* chore: update dependencies and package versions
- Bumped `jest-environment-jsdom` to version 30.2.0 in both package.json and client/package.json.
- Updated related Jest packages to version 30.2.0 in package-lock.json, ensuring compatibility with the latest features and fixes.
- Added `svgo` package with version 2.8.2 to package.json for improved SVG optimization.
* chore: add @happy-dom/jest-environment and update test files
- Added `@happy-dom/jest-environment` version 20.8.3 to `package.json` and `package-lock.json` for improved testing environment.
- Updated test files to utilize the new Jest environment, replacing mock implementations of `window.location` with `window.history.replaceState` for better clarity and maintainability.
- Refactored tests in `SourcesErrorBoundary`, `useFocusChatEffect`, `AuthContext`, and `StartupLayout` to enhance reliability and reduce complexity.
* 🔧 fix: Use longest-match in findMatchingPattern, remove deprecated PaLM2/Codey models
findMatchingPattern now selects the longest matching key instead of the
first reverse-order match, preventing cross-provider substring collisions
(e.g., "gpt-5.2-chat-2025-12-11" incorrectly matching Google's "chat-"
pattern instead of OpenAI's "gpt-5.2"). Adds early exit when key length
equals model name length. Reorders aggregateModels spreads so OpenAI is
last (preferred on same-length ties). Removes deprecated PaLM2/Codey
entries from googleModels.
* refactor: re-order models based on more likely usage
* refactor: Improve key matching logic in findMatchingPattern
Updated the findMatchingPattern function to enhance key matching by ensuring case-insensitive comparisons and maintaining the longest match priority. Clarified comments regarding key ordering and performance implications, emphasizing the importance of defining older models first for efficiency and the handling of same-length ties. This refactor aims to improve code clarity and maintainability.
* test: Enhance findMatchingPattern tests for edge cases and performance
Added new test cases to the findMatchingPattern function, covering scenarios such as empty model names, case-insensitive matching, and performance optimizations. Included checks for longest match priority and ensured deprecated PaLM2/Codey models are no longer present in token entries. This update aims to improve test coverage and validate the function's behavior under various conditions.
* test: Update findMatchingPattern test to use last key for exact match validation
Modified the test for findMatchingPattern to utilize the last key from the openAIMap for exact match checks, ensuring the test accurately reflects the expected behavior of the function. This change enhances the clarity and reliability of the test case.
* chore: Update logging format for tool execution handler to improve clarity
* fix: Expand toolkit tools in loadToolDefinitions for event-driven mode
The image_gen_oai toolkit contains both image_gen_oai and image_edit_oai
tools, but the definitions-only path only returned image_gen_oai. This
adds toolkit expansion so child tools are included in definitions, and
resolves child tool names to their parent toolkit constructor at runtime.
* chore: Remove toolkit flag from gemini_image_gen
gemini_image_gen only has a single tool, so it is not a true toolkit.
* refactor: Address review findings for toolkit expansion
- Guard against duplicate constructor calls when parent and child tools
are both in the tools array (Finding 2)
- Consolidate image tool descriptions/schemas — registry now derives from
toolkit objects (oaiToolkit, geminiToolkit) instead of duplicating them,
so env var overrides are respected everywhere (Finding 5)
- Move toolkitExpansion/toolkitParent to toolkits/mapping.ts with
immutable types (Findings 6, 9)
- Add tests for toolkit expansion, deduplication, and mapping
invariants (Finding 1)
- Fix log format to quote each tool individually (Finding 8)
* fix: Correct toolkit constructor lookup to store under requested tool name
The previous dedup guard stored the factory under toolKey (parent name)
instead of tool (requested name), causing the promise loop to miss
child tools like image_edit_oai. Now stores under both the parent key
(for dedup) and the requested name (for lookup), with a memoized
factory to ensure the constructor runs only once.
- Replaced external icon URLs in manifest.json with local SVG assets for Google Search, DALL-E-3, Tavily Search, Calculator, Stable Diffusion, Azure AI Search, and Flux.
- Added new SVG files for Google Search, DALL-E-3, Tavily, Calculator, Stable Diffusion, and Azure AI Search to the assets directory, enhancing performance and reliability by using local resources.
* refactor: consistent theming between inline and Artifacts Mermaid Diagram
* refactor: Enhance Mermaid component with improved theming and security features
- Updated Mermaid component to utilize useCallback for performance optimization.
- Increased maximum zoom level from 4 to 10 for better diagram visibility.
- Added security level configuration to Mermaid initialization for enhanced security.
- Refactored theme handling to ensure consistent theming between inline and artifact diagrams.
- Introduced unit tests for Mermaid configuration to validate flowchart settings and theme behavior.
* refactor: Improve theme handling in useMermaid hook
- Enhanced theme variable management by merging custom theme variables with default values for dark mode.
- Ensured consistent theming across Mermaid diagrams by preserving existing theme configurations while applying new defaults.
* refactor: Consolidate imports in mermaid test file
- Combined multiple imports from the mermaid utility into a single statement for improved readability and organization in the test file.
* feat: Add subgraph title contrast adjustment for Mermaid diagrams
- Introduced a utility function to enhance text visibility on subgraph titles by adjusting the fill color based on background luminance.
- Updated the Mermaid component to utilize this function, ensuring better contrast in rendered SVGs.
- Added comprehensive unit tests to validate the contrast adjustment logic across various scenarios.
* refactor: Update MermaidHeader component for improved button accessibility and styling
- Replaced Button components with TooltipAnchor for better accessibility and user experience.
- Consolidated button styles into a single class for consistency.
- Enhanced the layout and spacing of the header for a cleaner appearance.
* fix: hex color handling and improve contrast adjustment in Mermaid diagrams
- Updated hexLuminance function to support 3-character hex shorthand by expanding it to 6 characters.
- Refined the fixSubgraphTitleContrast function to avoid double semicolons in style attributes and ensure proper fill color adjustments based on background luminance.
- Added unit tests to validate the handling of 3-character hex fills and the prevention of double semicolons in text styles.
* chore: Simplify Virtual Scrolling Performance tests by removing performance timing checks
- Removed performance timing checks and associated console logs from tests handling 1000 and 5000 agents.
- Focused tests on verifying the correct rendering of virtual list items without measuring render time.
When running inside K8s, requests to https://hanzo.id go through
Cloudflare which returns the CF Pages HTML instead of proxying to
IAM. This causes openid-client to fail with "unexpected response
content-type" since it gets HTML instead of JSON.
The fix adds URL rewriting in customFetch: when IAM_INTERNAL_URL is
set, all requests to OPENID_ISSUER (https://hanzo.id) are rewritten
to the internal IAM service (http://iam.hanzo.svc.cluster.local).
The OIDC issuer stays https://hanzo.id (matching the discovery
response), only the network path changes.
When OPENID_ISSUER uses http:// (internal K8s service), enable
openid-client's allowInsecureRequests to bypass the HTTPS-only check.
This avoids Cloudflare SSL 525 errors when discovering OIDC config
from within the cluster.
* fix(mcp): respect server's token endpoint auth method preference order
* fix(mcp): update token endpoint auth method to client_secret_basic
* fix(mcp): correct auth method to client_secret_basic in OAuth handler
* test(mcp): add tests for OAuth client registration method selection based on server preferences
* refactor(mcp): extract and implement token endpoint auth methods into separate utility functions
- Moved token endpoint authentication method logic from the MCPOAuthHandler to new utility functions in methods.ts for better organization and reusability.
- Added tests for the new methods to ensure correct behavior in selecting and resolving authentication methods based on server preferences and token exchange methods.
- Updated MCPOAuthHandler to utilize the new utility functions, improving code clarity and maintainability.
* chore(mcp): remove redundant comments in OAuth handler
- Cleaned up the MCPOAuthHandler by removing unnecessary comments related to authentication methods, improving code readability and maintainability.
* refactor(mcp): update supported auth methods to use ReadonlySet for better performance
- Changed the SUPPORTED_AUTH_METHODS from an array to a ReadonlySet for improved lookup efficiency.
- Enhanced the logic in selectRegistrationAuthMethod to prioritize credential-based methods and handle cases where the server advertises 'none' correctly, ensuring compliance with RFC 7591.
* test(mcp): add tests for selectRegistrationAuthMethod to handle 'none' and empty array cases
- Introduced new test cases to ensure selectRegistrationAuthMethod correctly prioritizes credential-based methods over 'none' when listed first or before other methods.
- Added a test to verify that an empty token_endpoint_auth_methods_supported returns undefined, adhering to RFC 8414.
* refactor(mcp): streamline authentication method handling in OAuth handler
- Simplified the logic for determining the authentication method by consolidating checks into a single function call.
- Removed redundant checks for supported auth methods, enhancing code clarity and maintainability.
- Updated the request header and body handling based on the resolved authentication method.
* fix(mcp): ensure compliance with RFC 6749 by removing credentials from body when using client_secret_basic
- Updated the MCPOAuthHandler to delete client_id and client_secret from body parameters when using the client_secret_basic authentication method, ensuring adherence to RFC 6749 §2.3.1.
* test(mcp): add tests for OAuth flow handling of client_secret_basic and client_secret_post methods
- Introduced new test cases to verify that the MCPOAuthHandler correctly removes client_id and client_secret from the request body when using client_secret_basic.
- Added tests to ensure proper handling of client_secret_post and none authentication methods, confirming that the correct parameters are included or excluded based on the specified method.
- Enhanced the test suite for completeOAuthFlow to cover various scenarios, ensuring compliance with OAuth 2.0 specifications.
* test(mcp): enhance tests for selectRegistrationAuthMethod and resolveTokenEndpointAuthMethod
- Added new test cases to verify the selection of the first supported credential method from a mixed list in selectRegistrationAuthMethod.
- Included tests to ensure resolveTokenEndpointAuthMethod correctly ignores unsupported preferred methods and handles empty tokenAuthMethods, returning undefined as expected.
- Improved test coverage for various scenarios in the OAuth flow, ensuring compliance with relevant specifications.
---------
Co-authored-by: Dustin Healy <54083382+dustinhealy@users.noreply.github.com>
- Bumped `underscore` version from 1.13.7 to 1.13.8 to incorporate the latest improvements and fixes.
- Updated package-lock.json to reflect the new version and ensure consistency across dependencies.
* feat: allow editors to duplicate agents
* fix: Update permissions for duplicating agents and enhance visibility in AgentFooter
- Changed required permission for duplicating agents from VIEW to EDIT in the API route.
- Updated AgentFooter component to display the duplicate button for admins and users with EDIT permission, improving access control.
- Added tests to ensure the duplicate button visibility logic works correctly based on user roles and permissions.
* test: Update AgentFooter tests to reflect permission changes
- Adjusted tests in AgentFooter.spec.tsx to verify UI behavior based on user permissions.
- Updated expectations for the visibility of the grant access dialog and duplicate button, ensuring they align with the new permission logic.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 💰 feat: Add gpt-5.3 context window and pricing
* 💰 feat: Add OpenAI cached input pricing and `gpt-5.2-pro` model
- Add cached input pricing (write/read) for gpt-4o, gpt-4.1, gpt-5.x,
o1, o3, o4-mini models with correct per-family discount tiers
- Add gpt-5.2-pro pricing ($21/$168), context window, and max output
- Pro models (gpt-5-pro, gpt-5.2-pro) correctly excluded from cache
pricing as OpenAI does not support caching for these
* 🔍 fix: Address review findings for OpenAI pricing
- Add o1-preview to cacheTokenValues (50% discount, same as o1)
- Fix comment to enumerate all models per discount tier
- Add cache tests for dated variants (gpt-4o-2024-08-06, etc.)
- Add gpt-5-mini/gpt-5-nano to 10% ratio invariant test
- Replace forEach with for...of in new test code
- Fix inconsistent test description phrasing
- Add gpt-5.3-preview to context window tests
- Changed the text for the "No Auth" message to "None (Auto-detect)" in the English translation file, enhancing clarity for users regarding authentication status.
* feat: Allow Credential Variables in Headers for DB-sourced MCP Servers
- Removed the hasCustomUserVars check from ToolService.js, directly retrieving userMCPAuthMap.
- Updated MCPConnectionFactory and related classes to include a dbSourced flag for better handling of database-sourced configurations.
- Added integration tests to ensure proper behavior of dbSourced servers, verifying that sensitive placeholders are not resolved while allowing customUserVars.
- Adjusted various MCP-related files to accommodate the new dbSourced logic, ensuring consistent handling across the codebase.
* chore: MCPConnectionFactory Tests with Additional Flow Metadata for typing
- Updated MCPConnectionFactory tests to include new fields in flowMetadata: serverUrl and state.
- Enhanced mockFlowData in multiple test cases to reflect the updated structure, ensuring comprehensive coverage of the OAuth flow scenarios.
- Added authorization_endpoint to metadata in the test setup for improved validation of the OAuth process.
* refactor: Simplify MCPManager Configuration Handling
- Removed unnecessary type assertions and streamlined the retrieval of server configuration in MCPManager.
- Enhanced the handling of OAuth and database-sourced flags for improved clarity and efficiency.
- Updated tests to reflect changes in user object structure and ensure proper processing of MCP environment variables.
* refactor: Optimize User MCP Auth Map Retrieval in ToolService
- Introduced conditional loading of userMCPAuthMap based on the presence of MCP-delimited tools, improving efficiency by avoiding unnecessary calls.
- Updated the loadToolDefinitionsWrapper and loadAgentTools functions to reflect this change, enhancing overall performance and clarity.
* test: Add userMCPAuthMap gating tests in ToolService
- Introduced new tests to validate the logic for determining if MCP tools are present in the agent's tool list.
- Implemented various scenarios to ensure accurate detection of MCP tools, including edge cases for empty, undefined, and null tool lists.
- Enhanced clarity and coverage of the ToolService capability checking logic.
* refactor: Enhance MCP Environment Variable Processing
- Simplified the handling of the dbSourced parameter in the processMCPEnv function.
- Introduced a failsafe mechanism to derive dbSourced from options if not explicitly provided, improving robustness and clarity in MCP environment variable processing.
* refactor: Update Regex Patterns for Credential Placeholders in ServerConfigsDB
- Modified regex patterns to include additional credential/env placeholders that should not be allowed in user-provided configurations.
- Clarified comments to emphasize the security risks associated with credential exfiltration when MCP servers are shared between users.
* chore: field order
* refactor: Clean Up dbSourced Parameter Handling in processMCPEnv
- Reintroduced the failsafe mechanism for deriving the dbSourced parameter from options, ensuring clarity and robustness in MCP environment variable processing.
- Enhanced code readability by maintaining consistent comment structure.
* refactor: Update MCPOptions Type to Include Optional dbId
- Modified the processMCPEnv function to extend the MCPOptions type, allowing for an optional dbId property.
- Simplified the logic for deriving the dbSourced parameter by directly checking the dbId property, enhancing code clarity and maintainability.
* 🤖 feat: `gemini-3.1-flash-lite-preview` Window & Pricing
- Updated `.env.example` to include `gemini-3.1-flash-lite-preview` in the list of available models.
- Enhanced `tx.js` to define token values for `gemini-3.1-flash-lite`.
- Adjusted `tokens.ts` to allocate input tokens for `gemini-3.1-flash-lite`.
- Modified `config.ts` to include `gemini-3.1-flash-lite-preview` in the default models list.
* chore: testing for `gemini-3.1-flash-lite` model, comments
- Updated `tx.js` to include cache token values for `gemini-3.1-flash-lite` with specific write and read rates.
- Enhanced `tx.spec.js` to include tests for the new `gemini-3.1-flash-lite-preview` model, ensuring correct rate retrieval for both prompt and completion token types.
* ♻️ refactor: Centralize `buildLoginRedirectUrl` in data-provider
Move `buildLoginRedirectUrl` from `client/src/utils/redirect.ts` into
`packages/data-provider/src/api-endpoints.ts` so the axios 401
interceptor (and any other data-provider consumer) can use the canonical
implementation with the LOGIN_PATH_RE guard and BASE_URL awareness.
The client module now re-exports from `librechat-data-provider`, keeping
all existing imports working unchanged.
* 🔒 fix: Shared link 401 interceptor bypass and redirect loop (#12033)
Fixes three issues in the axios 401 response interceptor that prevented
private shared links (ALLOW_SHARED_LINKS_PUBLIC=false) from working:
1. `window.location.href.includes('share/')` matched the full URL
(including query params and hash), causing false positives. Changed
to `window.location.pathname.startsWith('/share/')`.
2. When token refresh returned no token on a share page, the
interceptor logged and fell through without redirecting, causing an
infinite retry loop via React Query. Now redirects to login using
`buildLoginRedirectUrl()` which preserves the share URL for
post-login navigation.
3. `processQueue` was never called in the no-token branch, leaving
queued requests with dangling promise callbacks. Added
`processQueue(error, null)` before the redirect.
* ✅ test: Comprehensive 401 interceptor tests for shared link auth flow
Rewrite interceptor test suite to cover all shared link auth scenarios:
- Unauthenticated user on share page with failed refresh → redirect
- Authenticated user on share page with failed refresh → redirect
- share/ in query params does NOT bypass the auth header guard
- Login path guard: redirect to plain /login (no redirect_to loop)
- Refresh success: assert exact call count (toBe(3) vs toBeGreaterThan)
Test reliability improvements:
- window.location teardown moved to afterEach (no state leak on failure)
- expect.assertions(N) on all tests (catch silent false passes)
- Shared setWindowLocation helper for consistent location mocking
* ♻️ refactor: Import `buildLoginRedirectUrl` directly from data-provider
Update `AuthContext.tsx` and `useAuthRedirect.ts` to import
`buildLoginRedirectUrl` from `librechat-data-provider` instead of
re-exporting through `~/utils/redirect.ts`.
Convert `redirect.ts` to ESM-style inline exports and remove the
re-export of `buildLoginRedirectUrl`.
* ✅ test: Move `buildLoginRedirectUrl` tests to data-provider
Tests for `buildLoginRedirectUrl` now live alongside the implementation
in `packages/data-provider/specs/api-endpoints.spec.ts`.
Removed the duplicate describe block from the client redirect test file
since it no longer owns that function.
* 🔧 chore: Update @vitejs/plugin-react to version 5.1.4 and clean up package-lock.json
- Upgraded @vitejs/plugin-react from version 4.3.4 to 5.1.4 in both package.json and package-lock.json.
- Removed unused dependencies related to previous plugin versions from package-lock.json.
- Updated @babel/compat-data to version 7.29.0 and added new dependencies for Babel plugins.
* 🔧 chore: Upgrade vite-plugin-pwa to version 1.2.0 in package.json and package-lock.json
- Updated vite-plugin-pwa from version 0.21.2 to 1.2.0 in both package.json and package-lock.json to ensure compatibility with the latest features and improvements.
- Removed outdated dependency entries related to the previous version from package-lock.json.
* 🔧 chore: Upgrade vite to version 7.3.1 in package.json and package-lock.json
- Updated vite from version 6.4.1 to 7.3.1 in both package.json and package-lock.json to leverage new features and improvements.
- Added new esbuild packages for various architectures in package-lock.json to support broader compatibility.
* 🔧 chore: Update @babel dependencies and vite-plugin-node-polyfills version in package.json and package-lock.json
- Upgraded vite-plugin-node-polyfills from version 0.23.0 to 0.25.0 for improved compatibility.
- Added several new @babel packages and updated existing ones to version 7.29.0 and 7.28.6, enhancing Babel's functionality and support.
- Removed outdated semver entries from package-lock.json to streamline dependencies.
* 🔧 chore: Vite configuration with node polyfills resolver and clean up imports
- Added a custom resolver for node polyfills shims to improve compatibility with legacy modules.
- Cleaned up import statements by removing unnecessary comments and organizing imports for better readability.
- Utilized `createRequire` to handle module resolution in a more efficient manner.
* 🔧 chore: Upgrade fast-xml-parser to version 5.3.8 in package.json and package-lock.json
- Updated fast-xml-parser from version 5.3.6 to 5.3.8 in both package.json and package-lock.json to incorporate the latest features and improvements.
- Ensured consistency across dependencies by aligning the version in all relevant files.
* 🔧 chore: Upgrade @types/node to version 20.19.35 in package.json and package-lock.json
- Updated @types/node from version 20.3.0 to 20.19.35 in both package.json and package-lock.json to ensure compatibility with the latest TypeScript features and improvements.
* 🔧 chore: Vite configuration to centralize node polyfills shims
- Moved node polyfills shims into a dedicated constant for improved readability and maintainability.
- Updated the custom resolver to utilize the new centralized shims, enhancing compatibility with legacy modules.
- Added documentation to clarify the purpose of the node polyfills shims mapping.
package.json declares packageManager: pnpm@10.27.0 but CI workflows
specified conflicting versions (test.yml had v8, others had "latest").
pnpm/action-setup@v4 reads packageManager automatically when no
version is specified, making package.json the single source of truth.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
package.json declares packageManager: pnpm@10.27.0 but CI workflows
specified conflicting versions (test.yml had v8, others had "latest").
pnpm/action-setup@v4 reads packageManager automatically when no
version is specified, making package.json the single source of truth.
* 🔗 fix: Preserve URL query params during silent token refresh
The silent token refresh on hard navigation was redirecting to '/c/new'
without query params, wiping the URL before ChatRoute could read them.
Now preserves the current URL (pathname + search) as the redirect
fallback, with isSafeRedirect validation.
* 🧭 fix: Apply URL query params in ChatRoute initialization
ChatRoute now reads URL search params (endpoint, model, agent_id, etc.)
and merges them into the preset passed to newConversation(), so the
first conversation init already includes the URL param settings. This
eliminates the race where useQueryParams fired too late.
- Export processValidSettings from useQueryParams for reuse
- Add getNewConvoPreset helper in ChatRoute (used in both NEW_CONVO branches)
- Query params take precedence over model spec defaults
- useQueryParams now waits for endpointsConfig before processing
- Skip redundant newQueryConvo when settings are already applied
- Clean all URL params via setSearchParams after processing
* ✅ test: Update useQueryParams tests for new URL cleanup behavior
- Assert setSearchParams called instead of window.history.replaceState
- Mock endpoints config in deferred submission and timeout tests
* ♻️ refactor: Move processValidSettings to ~/utils and address review findings
- Move processValidSettings/parseQueryValue to createChatSearchParams.ts
(pure utility, not hook-specific)
- Fix processSubmission: use setSearchParams instead of replaceState,
move URL cleanup outside data.text guard
- Narrow endpointsConfig guard: only block settings application, not
prompt-only flows
- Convert areSettingsApplied to stable useCallback ([] deps) with
conversationRef to avoid interval churn on conversation updates
- Replace console.log with logger.log in production paths
- Restore explanatory comment on pendingSubmitRef guard
- Use for...of in processValidSettings (CLAUDE.md preference)
- Remove unused imports from useQueryParams
* 🔧 fix: Add areSettingsApplied to effect deps and fix test mocks
- Restore areSettingsApplied in main effect deps (stable identity with
[] deps, safe to include — satisfies exhaustive-deps lint rule)
- Fix all test getQueryData mocks to properly distinguish between
startupConfig and endpoints keys
- Assert setSearchParams call arguments (URLSearchParams + replace:true)
* ✅ test: Assert empty URLSearchParams in setSearchParams calls
Tighten setSearchParams assertions to verify the params are empty
(toString() === ''), not just that a URLSearchParams instance was passed.
* 🔧 test: Update AuthContext tests to navigate to current URL for redirects
- Modified test cases to assert navigation to the current URL instead of a hardcoded '/c/new' when no stored redirect exists or when falling back from unsafe stored redirects.
- Enhanced test setup to define window.location for accurate simulation of redirect behavior.
* chore: Update import path for StartupLayout in tests
* 🔒 fix: Enhance AuthContext to handle stored redirects during user authentication
- Added SESSION_KEY import and logic to retrieve and clear stored redirect URLs from sessionStorage.
- Updated user context state to include redirect URL, defaulting to '/c/new' if none is found.
* 🧪 test: Add tests for silentRefresh post-login redirect handling in AuthContext
- Introduced new test suite to validate navigation behavior after successful token refresh.
- Implemented tests for stored sessionStorage redirects, default navigation, and prevention of unsafe redirects.
- Enhanced logout error handling tests to ensure proper state clearing without external redirects.
* 🔒 fix: Update AuthContext to handle unsafe stored redirects during authentication
- Removed conditional check for stored redirect in sessionStorage, ensuring it is always cleared.
- Enhanced logic to validate stored redirects, defaulting to '/c/new' for unsafe URLs.
- Updated tests to verify navigation behavior for both safe and unsafe redirects after token refresh.
- Changed image references from `ghcr.io` to `registry.librechat.ai` across multiple Docker and Helm files, ensuring consistency in image sourcing.
- Updated `deploy-compose.yml`, `docker-compose.override.yml.example`, `docker-compose.yml`, `rag.yml`, and various Helm chart files to reflect the new registry.
* fix: key checkmark by endpoint , not just model name
* fix: model spec endpoint collision for checkmark indicators
* chore: fix formatting
* refactor: move isSelected into EndpointModelItem, fix SearchResults, add tests
Address PR review feedback:
- Move isSelected computation from renderEndpointModels into EndpointModelItem
via useModelSelectorContext, eliminating fragile positional params
- Add !selectedSpec guard to SearchResults.tsx for both model and endpoint checks
- Add unit tests for EndpointModelItem selection logic
* test: update EndpointModelItem tests and add SearchResults tests
- Update EndpointModelItem tests to replace null modelSpec with an empty string for consistency in rendering logic.
- Introduce new SearchResults tests to validate selection behavior based on endpoint and model matching, including scenarios with and without active specs.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: complete OIDC logout implementation
The OIDC logout feature added in #5626 was incomplete:
1. Backend: Missing id_token_hint/client_id parameters required by the
RP-Initiated Logout spec. Keycloak 18+ rejects logout without these.
2. Frontend: The logout redirect URL was passed through isSafeRedirect()
which rejects all absolute URLs. The redirect was silently dropped.
Backend: Add id_token_hint (preferred) or client_id (fallback) to the
logout URL for OIDC spec compliance.
Frontend: Use window.location.replace() for logout redirects from the
backend, bypassing isSafeRedirect() which was designed for user-input
validation.
Fixes#5506
* fix: accept undefined in setTokenHeader to properly clear Authorization header
When token is undefined, delete the Authorization header instead of
setting it to "Bearer undefined". Removes the @ts-ignore workaround
in AuthContext.
* fix: skip axios 401 refresh when Authorization header is cleared
When the Authorization header has been removed (e.g. during logout),
the response interceptor now skips the token refresh flow. This
prevents a successful refresh from canceling an in-progress OIDC
external redirect via window.location.replace().
* fix: guard against undefined OPENID_CLIENT_ID in logout URL
Prevent literal "client_id=undefined" in the OIDC end-session URL
when OPENID_CLIENT_ID is not set. Log a warning when neither
id_token_hint nor client_id is available.
* fix: prevent race condition canceling OIDC logout redirect
The logout mutation wrapper's cleanup (clearStates, removeQueries)
triggers re-renders and 401s on in-flight requests. The axios
interceptor would refresh the token successfully, firing
dispatchTokenUpdatedEvent which cancels the window.location.replace()
navigation to the IdP's end_session_endpoint.
Fix:
- Clear Authorization header synchronously before redirect so the
axios interceptor skips refresh for post-logout 401s
- Add isExternalRedirectRef to suppress silentRefresh and useEffect
side effects during the redirect
- Add JSDoc explaining why isSafeRedirect is bypassed
* test: add LogoutController and AuthContext logout test coverage
LogoutController.spec.js (13 tests):
- id_token_hint from session and cookie fallback
- client_id fallback, including undefined OPENID_CLIENT_ID guard
- Disabled endpoint, missing issuer, non-OpenID user
- post_logout_redirect_uri (custom and default)
- Missing OpenID config and end_session_endpoint
- Error handling and cookie clearing
AuthContext.spec.tsx (3 tests):
- OIDC redirect calls window.location.replace + setTokenHeader
- Non-redirect logout path
- Logout error handling
* test: add coverage for setTokenHeader, axios interceptor guard, and silentRefresh suppression
headers-helpers.spec.ts (3 tests):
- Sets Authorization header with Bearer token
- Deletes Authorization header when called with undefined
- No-op when clearing an already absent header
request-interceptor.spec.ts (2 tests):
- Skips refresh when Authorization header is cleared (the race fix)
- Attempts refresh when Authorization header is present
AuthContext.spec.tsx (1 new test):
- Verifies silentRefresh is not triggered after OIDC redirect
* test: enhance request-interceptor tests with adapter restoration and refresh verification
- Store the original axios adapter before tests and restore it after all tests to prevent side effects.
- Add verification for the refresh endpoint call in the interceptor tests to ensure correct behavior during token refresh attempts.
* test: enhance AuthContext tests with live rendering and improved logout error handling
- Introduced a new `renderProviderLive` function to facilitate testing with silentRefresh.
- Updated tests to use the live rendering function, ensuring accurate simulation of authentication behavior.
- Enhanced logout error handling test to verify that auth state is cleared without external redirects.
* test: update LogoutController tests for OpenID config error handling
- Renamed test suite to clarify that it handles cases when OpenID config is not available.
- Modified test to check for error thrown by getOpenIdConfig instead of returning null, ensuring proper logging of the error message.
* refactor: improve OpenID config error handling in LogoutController
- Simplified error handling for OpenID configuration retrieval by using a try-catch block.
- Updated logging to provide clearer messages when the OpenID config is unavailable.
- Ensured that the end session endpoint is only accessed if the OpenID config is successfully retrieved.
---------
Co-authored-by: cloudspinner <stijn.tastenhoye@gmail.com>
- Set Conversations list role as "rowgroup", which better describes
what is actually going on than "row".
- Increased contrast on placeholder text in ChatForm.
The drag & drop background was practically translucent, which made
it hard to see the rest of the overlay (especially on light mode).
Now, we no longer make the background translucent, so you can see
the overlay clearly.
When content is hidden (or in the background of the active form),
users shouldn't be allowed to access that content. However, right now,
you can use a keyboard (or screen reader) to move over to this content.
By adding `inert`, we make this content no longer accessible when hidden.
I've done this in two places:
- The sidebar is now inert when it's closed.
- When the sidebar is open & the window is small, the content area is
inert (since it's mostly obscured by the sidebar).
* 🔌 fix: Resolve MCP OAuth flow state race condition
The OAuth callback arrives before the flow state is stored because
`createFlow()` returns a long-running Promise that only resolves on
flow COMPLETION, not when the initial PENDING state is persisted.
Calling it fire-and-forget with `.catch(() => {})` meant the redirect
happened before the state existed, causing "Flow state not found"
errors.
Changes:
- Add `initFlow()` to FlowStateManager that stores PENDING state and
returns immediately, decoupling state persistence from monitoring
- Await `initFlow()` before emitting the OAuth redirect so the
callback always finds existing state
- Keep `createFlow()` in the background for monitoring, but log
warnings instead of silently swallowing errors
- Increase FLOWS cache TTL from 3 minutes to 10 minutes to give
users more time to complete OAuth consent screens
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* 🔌 refactor: Revert FLOWS cache TTL change
The race condition fix (initFlow) is sufficient on its own.
TTL configurability should be a separate enhancement via
librechat.yaml mcpSettings rather than a hardcoded increase.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* 🔌 fix: Address PR review — restore FLOWS TTL, fix blocking-path race, clean up dead args
- Restore FLOWS cache TTL to 10 minutes (was silently dropped back to 3)
- Add initFlow before oauthStart in blocking handleOAuthRequired path
to guarantee state persistence before any redirect
- Pass {} to createFlow metadata arg (dead after initFlow writes state)
- Downgrade background monitor .catch from logger.warn to logger.debug
- Replace process.nextTick with Promise.resolve in test (correct semantics)
- Add initFlow TTL assertion test
- Add blocking-path ordering test (initFlow → oauthStart → createFlow)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(data-provider): include timezone and weekday label in current_datetime
* fix(data-provider): use named weekday for both date variables and single dayjs instance
Use a single `const now = dayjs()` instead of 5 separate instantiations,
apply named weekday to `{{current_date}}` (not just `{{current_datetime}}`),
simplify weekday format from `(weekday=Monday)` to `(Monday)`, and
harden test mock fallback to throw on unhandled format strings.
* chore(data-provider): remove dead day() mock from parsers spec
---------
Co-authored-by: Peter Rothlaender <peter.rothlaender@ginkgo.com>
* 🔗 fix: Normalize MCP OAuth `resource` parameter to match token exchange
The authorization request used the raw resource string from metadata while
the token exchange normalized it through `new URL().href`, causing a
trailing-slash mismatch that Cloudflare's auth server rejected. Canonicalize
the resource URL in both paths so they match.
* 🔧 test: Simplify LeaderElection integration tests for Redis
Refactored the integration tests for LeaderElection with Redis by reducing the number of instances from 100 to 1, streamlining the leadership election process. Updated assertions to verify leadership status and UUID after resignation, improving test clarity and performance. Adjusted timeout to 15 seconds for the single instance scenario.
* 🔧 test: Update LeaderElection test case description for clarity
Modified the description of the test case for leader resignation in the LeaderElection integration tests to better reflect the expected behavior, enhancing clarity and understanding of the test's purpose.
* refactor: `resource` parameter in MCP OAuth authorization URL
Updated the `MCPOAuthHandler` to ensure the `resource` parameter is added to the authorization URL even when an error occurs while retrieving it from metadata. This change improves the handling of invalid resource URLs by using the raw value as a fallback, enhancing the robustness of the authorization process.
Split single-job Docker build into three jobs:
- build-amd64 on hanzo-build (docker-container driver, network=host)
- build-arm64 on hanzo-build-arm64 (native buildx, no QEMU)
- create-manifest on hanzo-build combining both digests
Uses registry cache per-arch and pushes to both Docker Hub and GHCR.
* 🪵 refactor: Simplify MCP Transport Log Messages
- Updated the logging in MCPConnection to provide clearer output by explicitly logging the method and ID of messages received and sent, improving traceability during debugging.
- This change replaces the previous JSON stringification of messages with a more structured log format, enhancing readability and understanding of the transport interactions.
* 🔧 refactor: Streamline MCPConnection Message Handling
- Removed redundant onmessage logging in MCPConnection to simplify the codebase.
- Introduced a dedicated setupTransportOnMessageHandler method to centralize message handling and improve clarity in transport interactions.
- Enhanced logging to provide clearer output for received messages, ensuring better traceability during debugging.
* 🔧 refactor: Rename setupTransportDebugHandlers to patchTransportSend
- Updated the MCPConnection class to rename the setupTransportDebugHandlers method to patchTransportSend for improved clarity.
- Adjusted the method call in the connection setup process to reflect the new naming, enhancing code readability and maintainability.
* fix: title sanitization with max length truncation and update ShareView for better text display
- Added functionality to `sanitizeTitle` to truncate titles exceeding 200 characters with an ellipsis, ensuring consistent title length.
- Updated `ShareView` component to apply a line clamp on the title, improving text display and preventing overflow in the UI.
* refactor: Update layout and styling in MessagesView and ShareView components
- Removed unnecessary padding in MessagesView to streamline the layout.
- Increased bottom padding in the message container for better spacing.
- Enhanced ShareView footer positioning and styling for improved visibility.
- Adjusted section and div classes in ShareView for better responsiveness and visual consistency.
* fix: Correct title fallback and enhance sanitization logic in sanitizeTitle
- Updated the fallback title in sanitizeTitle to use DEFAULT_TITLE_FALLBACK instead of a hardcoded string.
- Improved title truncation logic to ensure proper handling of maximum length and whitespace, including edge cases for emoji and whitespace-only titles.
- Added tests to validate the new sanitization behavior, ensuring consistent and expected results across various input scenarios.
- Extract 'owner' (org), 'title', and 'tag' claims from Casdoor OIDC tokens
- Add organization, organizationTitle, organizationTag fields to User schema
- Add organization field to Conversation and Message schemas (indexed + meiliIndex)
- Propagate user's organization to conversations and messages on save
- Enables org-aware data isolation across the Hanzo platform
* refactor: transaction handling by integrating pricing and bulk write operations
- Updated `recordCollectedUsage` to accept pricing functions and bulk write operations, improving transaction management.
- Refactored `AgentClient` and related controllers to utilize the new transaction handling capabilities, ensuring better performance and accuracy in token spending.
- Added tests to validate the new functionality, ensuring correct behavior for both standard and bulk transaction paths.
- Introduced a new `transactions.ts` file to encapsulate transaction-related logic and types, enhancing code organization and maintainability.
* chore: reorganize imports in agents client controller
- Moved `getMultiplier` and `getCacheMultiplier` imports to maintain consistency and clarity in the import structure.
- Removed duplicate import of `updateBalance` and `bulkInsertTransactions`, streamlining the code for better readability.
* refactor: add TransactionData type and CANCEL_RATE constant to data-schemas
Establishes a single source of truth for the transaction document shape
and the incomplete-context billing rate constant, both consumed by
packages/api and api/.
* refactor: use proper types in data-schemas transaction methods
- Replace `as unknown as { tokenCredits }` with `lean<IBalance>()`
- Use `TransactionData[]` instead of `Record<string, unknown>[]`
for bulkInsertTransactions parameter
- Add JSDoc noting insertMany bypasses document middleware
- Remove orphan section comment in methods/index.ts
* refactor: use shared types in transactions.ts, fix bulk write logic
- Import CANCEL_RATE from data-schemas instead of local duplicate
- Import TransactionData from data-schemas for PreparedEntry/BulkWriteDeps
- Use tilde alias for EndpointTokenConfig import
- Pass valueKey through to getMultiplier
- Only sum tokenValue for balance-enabled docs in bulkWriteTransactions
- Consolidate two loops into single-pass map
* refactor: remove duplicate updateBalance from Transaction.js
Import updateBalance from ~/models (sourced from data-schemas) instead
of maintaining a second copy. Also import CANCEL_RATE from data-schemas
and remove the Balance model import (no longer needed directly).
* fix: test real spendCollectedUsage instead of IIFE replica
Export spendCollectedUsage from abortMiddleware.js and rewrite the test
file to import and test the actual function. Previously the tests ran
against a hand-written replica that could silently diverge from the real
implementation.
* test: add transactions.spec.ts and restore regression comments
Add 22 direct unit tests for transactions.ts financial logic covering
prepareTokenSpend, prepareStructuredTokenSpend, bulkWriteTransactions,
CANCEL_RATE paths, NaN guards, disabled transactions, zero tokens,
cache multipliers, and balance-enabled filtering.
Restore critical regression documentation comments in
recordCollectedUsage.spec.js explaining which production bugs the
tests guard against.
* fix: widen setValues type to include lastRefill
The UpdateBalanceParams.setValues type was Partial<Pick<IBalance,
'tokenCredits'>> which excluded lastRefill — used by
createAutoRefillTransaction. Widen to also pick 'lastRefill'.
* test: use real MongoDB for bulkWriteTransactions tests
Replace mock-based bulkWriteTransactions tests with real DB tests using
MongoMemoryServer. Pure function tests (prepareTokenSpend,
prepareStructuredTokenSpend) remain mock-based since they don't touch
DB. Add end-to-end integration tests that verify the full prepare →
bulk write → DB state pipeline with real Transaction and Balance models.
* chore: update @librechat/agents dependency to version 3.1.54 in package-lock.json and related package.json files
* test: add bulk path parity tests proving identical DB outcomes
Three test suites proving the bulk path (prepareTokenSpend/
prepareStructuredTokenSpend + bulkWriteTransactions) produces
numerically identical results to the legacy path for all scenarios:
- usage.bulk-parity.spec.ts: mirrors all legacy recordCollectedUsage
tests; asserts same return values and verifies metadata fields on
the insertMany docs match what spendTokens args would carry
- transactions.bulk-parity.spec.ts: real-DB tests using actual
getMultiplier/getCacheMultiplier pricing functions; asserts exact
tokenValue, rate, rawAmount and balance deductions for standard
tokens, structured/cache tokens, CANCEL_RATE, premium pricing,
multi-entry batches, and edge cases (NaN, zero, disabled)
- Transaction.spec.js: adds describe('Bulk path parity') that mirrors
7 key legacy tests via recordCollectedUsage + bulk deps against
real MongoDB, asserting same balance deductions and doc counts
* refactor: update llmConfig structure to use modelKwargs for reasoning effort
Refactor the llmConfig in getOpenAILLMConfig to store reasoning effort within modelKwargs instead of directly on llmConfig. This change ensures consistency in the configuration structure and improves clarity in the handling of reasoning properties in the tests.
* test: update performance checks in processAssistantMessage tests
Revise the performance assertions in the processAssistantMessage tests to ensure that each message processing time remains under 100ms, addressing potential ReDoS vulnerabilities. This change enhances the reliability of the tests by focusing on maximum processing time rather than relative ratios.
* test: fill parity test gaps — model fallback, abort context, structured edge cases
- usage.bulk-parity: add undefined model fallback test
- transactions.bulk-parity: add abort context test (txns inserted,
balance unchanged when balance not passed), fix readTokens type cast
- Transaction.spec: add 3 missing mirrors — balance disabled with
transactions enabled, structured transactions disabled, structured
balance disabled
* fix: deduct balance before inserting transactions to prevent orphaned docs
Swap the order in bulkWriteTransactions: updateBalance runs before
insertMany. If updateBalance fails (after exhausting retries), no
transaction documents are written — avoiding the inconsistent state
where transactions exist in MongoDB with no corresponding balance
deduction.
* chore: import order
* test: update config.spec.ts for OpenRouter reasoning in modelKwargs
Same fix as llm.spec.ts — OpenRouter reasoning is now passed via
modelKwargs instead of llmConfig.reasoning directly.
AccountSettings was using Select, but it makes more sense (for a11y)
to use Menu. The Select has the wrong role & behavior for the purpose
of AccountSettings; the "listbox" role it uses is for selecting
values in a form.
Menu matches the actual content better for screen readers; the
"menu" role is more appropriate for selecting one of a number of links.
* 🧠 feat: Add Thinking Level Config for Gemini 3 Models
- Introduced a new setting for 'thinking level' in the Google configuration, allowing users to control the depth of reasoning for Gemini 3 models.
- Updated translation files to include the new 'thinking level' label and description.
- Enhanced the Google LLM configuration to support the new 'thinking level' parameter, ensuring compatibility with both Google and Vertex AI providers.
- Added necessary schema and type definitions to accommodate the new setting across the data provider and API layers.
* test: Google LLM Configuration for Gemini 3 Models
- Added tests to validate default thinking configuration for Gemini 3 models, ensuring `thinkingConfig` is set correctly without `thinkingLevel`.
- Implemented logic to ignore `thinkingBudget` for Gemini 3+ models, confirming that it does not affect the configuration.
- Included a test to verify that `gemini-2.9-flash` is not classified as a Gemini 3+ model, maintaining expected behavior for earlier versions.
- Updated existing tests to ensure comprehensive coverage of the new configurations and behaviors.
* fix: Update translation for Google LLM thinking settings
- Revised descriptions for 'thinking budget' and 'thinking level' in the English translation file to clarify their applicability to different Gemini model versions.
- Ensured that the new descriptions accurately reflect the functionality and usage of the settings for Gemini 2.5 and 3 models.
* docs: Update comments for Gemini 3+ thinking configuration
- Added detailed comments in the Google LLM configuration to clarify the differences between `thinkingLevel` and `thinkingBudget` for Gemini 3+ models.
- Explained the necessity of `includeThoughts` in Vertex AI requests and how it interacts with `thinkingConfig` for improved understanding of the configuration logic.
* fix: Update comment for Gemini 3 model versioning
- Corrected comment in the configuration file to reflect the proper versioning for Gemini models, changing "Gemini 3.0 Models" to "Gemini 3 Models" for clarity and consistency.
* fix: Update thinkingLevel schema for Gemini 3 Models
- Removed nullable option from the thinkingLevel field in the tConversationSchema to ensure it is always defined when present, aligning with the intended configuration for Gemini 3 models.
* fix: Update OpenRouter reasoning handling in LLM configuration
- Modified the OpenRouter configuration to use a unified `reasoning` object instead of separate `reasoning_effort` and `include_reasoning` properties.
- Updated tests to ensure that `reasoning_summary` is excluded from the reasoning object and that the configuration behaves correctly based on the presence of reasoning parameters.
- Enhanced test coverage for OpenRouter-specific configurations, ensuring proper handling of various reasoning effort levels.
* refactor: Improve OpenRouter reasoning handling in LLM configuration
- Updated the handling of the `reasoning` object in the OpenRouter configuration to clarify the relationship between `reasoning_effort` and `include_reasoning`.
- Enhanced comments to explain the behavior of the `reasoning` object and its compatibility with legacy parameters.
- Ensured that the configuration correctly falls back to legacy behavior when no explicit reasoning effort is provided.
* test: Enhance OpenRouter LLM configuration tests
- Added a new test to verify the combination of web search plugins and reasoning object for OpenRouter configurations.
- Updated existing tests to ensure proper handling of reasoning effort levels and fallback behavior when reasoning_effort is unset.
- Improved test coverage for OpenRouter-specific configurations, ensuring accurate validation of reasoning parameters.
* chore: Update @librechat/agents dependency to version 3.1.53
- Bumped the version of @librechat/agents in package-lock.json and related package.json files to ensure compatibility with the latest features and fixes.
- Updated integrity hashes to reflect the new version.
* 🧠 feat: Add reasoning_effort configuration for Bedrock models
- Introduced a new `reasoning_effort` setting in the Bedrock configuration, allowing users to specify the reasoning level for supported models.
- Updated the input parser to map `reasoning_effort` to `reasoning_config` for Moonshot and ZAI models, ensuring proper handling of reasoning levels.
- Enhanced tests to validate the mapping of `reasoning_effort` to `reasoning_config` and to ensure correct behavior for various model types, including Anthropic models.
- Updated translation files to include descriptions for the new configuration option.
* chore: Update translation keys for Bedrock reasoning configuration
- Renamed translation key from `com_endpoint_bedrock_reasoning_config` to `com_endpoint_bedrock_reasoning_effort` for consistency with the new configuration setting.
- Updated the parameter settings to reflect the change in the description key, ensuring accurate mapping in the application.
* 🧪 test: Enhance bedrockInputParser tests for reasoning_config handling
- Added tests to ensure that stale `reasoning_config` is stripped when switching models from Moonshot to Meta and ZAI to DeepSeek.
- Included additional tests to verify that `reasoning_effort` values of "none", "minimal", and "xhigh" do not forward to `reasoning_config` for Moonshot and ZAI models.
- Improved coverage for the bedrockInputParser functionality to ensure correct behavior across various model configurations.
* feat: Introduce Bedrock reasoning configuration and update input parser
- Added a new `BedrockReasoningConfig` enum to define reasoning levels: low, medium, and high.
- Updated the `bedrockInputParser` to utilize the new reasoning configuration, ensuring proper handling of `reasoning_effort` values.
- Enhanced logic to validate `reasoning_effort` against the defined configuration values before assigning to `reasoning_config`.
- Improved code clarity with additional comments and refactored conditions for better readability.
* fix: Use correct endpoint for file validation in agent panel uploads
Agent panel file uploads (FileSearch, FileContext, Code/Files) were validating against the active conversation's endpoint config instead of the agents endpoint config. This caused incorrect file size limits when the active chat used a different endpoint.
Add endpointOverride option to useFileHandling so callers can specify the correct endpoint for validation independent of the active conversation.
* fix: Use agents endpoint config for agent panel file upload validation
Agent panel file uploads (FileSearch, FileContext, Code/Files) validated
against the active conversation's endpoint config instead of the agents
endpoint config. This caused wrong file size limits when the active chat
used a different endpoint.
Adds endpointOverride to useFileHandling so callers can specify the
correct endpoint for both validation and upload routing, independent of
the active conversation.
* test: Add unit tests for useFileHandling hook to validate endpoint overrides
Introduced comprehensive tests for the useFileHandling hook, ensuring correct behavior when using endpoint overrides for file validation and upload routing. The tests cover various scenarios, including fallback to conversation endpoints and proper handling of agent-specific configurations, enhancing the reliability of file handling in the application.
* 🔧 fix: Add skippedAgentIds tracking in initializeClient error handling
- Enhanced error handling in the initializeClient function to track agent IDs that are skipped during processing. This addition improves the ability to monitor and debug issues related to agent initialization failures.
* 🔧 fix: Update model assignment in BaseClient to use instance model
- Modified the model assignment in BaseClient to use `this.model` instead of `responseMessage.model`, clarifying that when using agents, the model refers to the agent ID rather than the model itself. This change improves code clarity and correctness in the context of agent usage.
* 🔧 test: Add tests for recordTokenUsage model assignment in BaseClient
- Introduced new test cases in BaseClient to ensure that the correct model is passed to the recordTokenUsage method, verifying that it uses this.model instead of the agent ID from responseMessage.model. This enhances the accuracy of token usage tracking in agent scenarios.
- Improved error handling in the initializeClient function to log errors when processing agents, ensuring that skipped agent IDs are tracked for better debugging.
* feat: Add messageId to transactions
* chore: field order
* feat: Enhance token usage tracking by adding messageId parameter
- Updated `recordTokenUsage` method in BaseClient to accept a new `messageId` parameter for improved tracking.
- Propagated `messageId` in the AgentClient when recording usage.
- Added tests to ensure `messageId` is correctly passed and handled in various scenarios, including propagation across multiple usage entries.
* chore: Correct field order in createGeminiImageTool function
- Moved the conversationId field to the correct position in the object being passed to the recordTokenUsage method, ensuring proper parameter alignment for improved functionality.
* refactor: Update OpenAIChatCompletionController and createResponse to use responseId instead of requestId
- Replaced instances of requestId with responseId in the OpenAIChatCompletionController for improved clarity in logging and tracking.
- Updated createResponse to include responseId in the requestBody, ensuring consistency across the handling of message identifiers.
* test: Add messageId to agent client tests
- Included messageId in the agent client tests to ensure proper handling and propagation of message identifiers during transaction recording.
- This update enhances the test coverage for scenarios involving messageId, aligning with recent changes in the tracking of message identifiers.
* fix: Update OpenAIChatCompletionController to use requestId for context
- Changed the context object in OpenAIChatCompletionController to use `requestId` instead of `responseId` for improved clarity and consistency in handling request identifiers.
* chore: field order
* feat: Implement 404 JSON response for unmatched API routes
- Added middleware to return a 404 JSON response with a message for undefined API routes.
- Updated SPA fallback to serve index.html for non-API unmatched routes.
- Ensured the error handler is positioned correctly as the last middleware in the stack.
* fix: Enhance logging in BaseClient for better token usage tracking
- Updated `getTokenCountForResponse` to log the messageId of the response for improved debugging.
- Enhanced userMessage logging to include messageId, tokenCount, and conversationId for clearer context during token count mapping.
* chore: Improve logging in processAddedConvo for better debugging
- Updated the logging structure in the processAddedConvo function to provide clearer context when processing added conversations.
- Removed redundant logging and enhanced the output to include model, agent ID, and endpoint details for improved traceability.
* chore: Enhance logging in BaseClient for improved token usage tracking
- Added debug logging in the BaseClient to track response token usage, including messageId, model, promptTokens, and completionTokens for better debugging and traceability.
* chore: Enhance logging in MemoryAgent for improved context
- Updated logging in the MemoryAgent to include userId, conversationId, messageId, and provider details for better traceability during memory processing.
- Adjusted log messages to provide clearer context when content is returned or not, aiding in debugging efforts.
* chore: Refactor logging in initializeClient for improved clarity
- Consolidated multiple debug log statements into a single message that provides a comprehensive overview of the tool context being stored for the primary agent, including the number of tools and the size of the tool registry. This enhances traceability and debugging efficiency.
* feat: Implement centralized 404 handling for unmatched API routes
- Introduced a new middleware function `apiNotFound` to standardize 404 JSON responses for undefined API routes.
- Updated the server configuration to utilize the new middleware, enhancing code clarity and maintainability.
- Added tests to ensure correct 404 responses for various non-GET methods and the `/api` root path.
* fix: Enhance logging in apiNotFound middleware for improved safety
- Updated the `apiNotFound` function to sanitize the request path by replacing problematic characters and limiting its length, ensuring safer logging of 404 errors.
* refactor: Move apiNotFound middleware to a separate file for better organization
- Extracted the `apiNotFound` function from the error middleware into its own file, enhancing code organization and maintainability.
- Updated the index file to export the new `notFound` middleware, ensuring it is included in the middleware stack.
* docs: Add comment to clarify usage of unsafeChars regex in notFound middleware
- Included a comment in the notFound middleware file to explain that the unsafeChars regex is safe to reuse with .replace() at the module scope, as it does not retain lastIndex state.
Replace real-looking JWT_SECRET and JWT_REFRESH_SECRET hex values with
empty placeholders. These should be generated per-deployment via
`openssl rand -hex 32` and stored in KMS, never in tracked files.
- Remove deepseek-r1-distill-70b and qwen3-32b from model lists
- Add zen3-nano, zen, zen4-coder-flash to Zen model catalog
- Reorder models by size (nano -> mini -> zen -> pro -> frontier)
- Replace OPENAI_API_KEY with HANZO_API_KEY for publishable key flow
- Replace Deepseek/Kimi/Moonshot display labels with Zen in parsers.ts
- Add HANZO_API_KEY env var to .env.example with IAM documentation
* 🔧 chore: Update Vite configuration to include additional package checks
- Enhanced the Vite configuration to recognize 'dnd-core' and 'flip-toolkit' alongside existing checks for 'react-dnd' and 'react-flip-toolkit' for improved handling of React interactions.
- Updated the markdown highlighting logic to also include 'lowlight' in addition to 'highlight.js' for better syntax highlighting support.
* 🔧 fix: Update AuthContextProvider to prevent infinite re-fire of useEffect
- Modified the dependency array of the useEffect hook in AuthContextProvider to an empty array, preventing unnecessary re-executions and potential infinite loops. Added an ESLint comment to clarify the decision regarding stable dependencies at mount.
* chore: import order
* 🔧 chore: bump minimatch due to ReDoS vulnerability
- Removed deprecated dependencies: @isaacs/balanced-match and @isaacs/brace-expansion.
- Upgraded Rollup packages from version 4.37.0 to 4.59.0 for improved performance and stability across multiple platforms.
* 🔧 chore: update Rollup version across multiple packages
- Bumped Rollup dependency from various versions to 4.34.9 in package.json and package-lock.json files for improved performance and compatibility across the project.
* 🔧 chore: update rimraf dependency to version 6.1.3 across multiple packages
- Bumped rimraf version from 6.1.2 to 6.1.3 in package.json and package-lock.json files for improved performance and compatibility.
* fix: Prevent recursive login redirect loop
buildLoginRedirectUrl() would blindly encode the current URL into a
redirect_to param even when already on /login, causing infinite nesting
(/login?redirect_to=%2Flogin%3Fredirect_to%3D...). Guard at source so
it returns plain /login when pathname starts with /login.
Also validates redirect_to in the login error handler with isSafeRedirect
to close an open-redirect vector, and removes a redundant /login guard
from useAuthRedirect now handled by the centralized check.
* 🔀 fix: Handle basename-prefixed login paths and remove double URL decoding
buildLoginRedirectUrl now uses isLoginPath() which matches /login,
/librechat/login, and /login/2fa — covering subdirectory deployments
where window.location.pathname includes the basename prefix.
Remove redundant decodeURIComponent calls on URLSearchParams.get()
results (which already returns decoded values) in getPostLoginRedirect,
Login.tsx, and AuthContext login error handler. The extra decode could
throw URIError on inputs containing literal percent signs.
* 🔀 fix: Tighten login path matching and add onError redirect tests
Replace overbroad `endsWith('/login')` with a single regex
`/(^|\/)login(\/|$)/` that matches `/login` only as a full path
segment. Unifies `isSafeRedirect` and `buildLoginRedirectUrl` to use
the same `LOGIN_PATH_RE` constant — no more divergent definitions.
Add tests for the AuthContext onError redirect_to preservation logic
(valid path preserved, open-redirect blocked, /login loop blocked),
and a false-positive guard proving `/c/loginhistory` is not matched.
Update JSDoc on `buildLoginRedirectUrl` to document the plain `/login`
early-return, and add explanatory comment in AuthContext `onError`
for why `buildLoginRedirectUrl()` cannot be used there.
* test: Add unit tests for AuthContextProvider login error handling
Introduced a new test suite for AuthContextProvider to validate the handling of login errors and the preservation of redirect parameters. The tests cover various scenarios including valid redirect preservation, open-redirect prevention, and recursive redirect prevention. This enhances the robustness of the authentication flow and ensures proper navigation behavior during login failures.
* ✨ feat: Add support for OpenDocument MIME types in file configuration
Updated the applicationMimeTypes regex to include support for OASIS OpenDocument formats, enhancing the file type recognition capabilities of the data provider.
* feat: document processing with OpenDocument support
Added support for OpenDocument Spreadsheet (ODS) MIME type in the file processing service and updated the document parser to handle ODS files. Included tests to verify correct parsing of ODS documents and updated file configuration to recognize OpenDocument formats.
* refactor: Enhance document processing to support additional Excel MIME types
Updated the document processing logic to utilize a regex for matching Excel MIME types, improving flexibility in handling various Excel file formats. Added tests to ensure correct parsing of new MIME types, including multiple Excel variants and OpenDocument formats. Adjusted file configuration to include these MIME types for better recognition in the file processing service.
* feat: Add support for additional OpenDocument MIME types in file processing
Enhanced the document processing service to support ODT, ODP, and ODG MIME types. Updated tests to verify correct routing through the OCR strategy for these new formats. Adjusted documentation to reflect changes in handled MIME types for improved clarity.
* fix: populate userMessage.files before first DB save
* fix: ESLint error fixed
* fix: deduplicate file-population logic and add test coverage
Extract `buildMessageFiles` helper into `packages/api/src/utils/message`
to replace three near-identical loops in BaseClient and both agent
controllers. Fixes set poisoning from undefined file_id entries, moves
file population inside the skipSaveUserMessage guard to avoid wasted
work, and adds full unit test coverage for the new behavior.
* chore: reorder import statements in openIdJwtStrategy.js for consistency
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* Allow setting the claim field to be used when OpenID login is configured
* fix(openid): harden getOpenIdEmail and expand test coverage
Guard against non-string claim values in getOpenIdEmail to prevent a
TypeError crash in isEmailDomainAllowed when domain restrictions are
configured. Improve warning messages to name the fallback chain
explicitly and distinguish missing vs. non-string claim values.
Fix the domain-block error log to record the resolved identifier rather
than userinfo.email, which was misleading when OPENID_EMAIL_CLAIM
resolved to a different field (e.g. upn).
Fix a latent test defect in openIdJwtStrategy.spec.js where the
~/server/services/Config mock exported getCustomConfig instead of
getAppConfig, the symbol actually consumed by openidStrategy.js.
Add refreshController tests covering the OPENID_EMAIL_CLAIM paths,
which were previously untested despite being a stated fix target.
Expand JWT strategy tests with null-payload, empty/whitespace
OPENID_EMAIL_CLAIM, migration-via-preferred_username, and call-order
assertions for the findUser lookup sequence.
* test(auth): enhance AuthController and openIdJwtStrategy tests for openidId updates
Added a new test in AuthController to verify that the openidId is updated correctly when a migration is triggered during the refresh process. Expanded the openIdJwtStrategy tests to include assertions for the updateUser function, ensuring that the correct parameters are passed when a user is found with a legacy email. This improves test coverage for OpenID-related functionality.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* added support for url query param persistance
* refactor: authentication redirect handling
- Introduced utility functions for managing login redirects, including `persistRedirectToSession`, `buildLoginRedirectUrl`, and `getPostLoginRedirect`.
- Updated `Login` and `AuthContextProvider` components to utilize these utilities for improved redirect logic.
- Refactored `useAuthRedirect` to streamline navigation to the login page while preserving intended destinations.
- Cleaned up the `StartupLayout` to remove unnecessary redirect handling, ensuring a more straightforward navigation flow.
- Added a new `redirect.ts` file to encapsulate redirect-related logic, enhancing code organization and maintainability.
* fix: enhance safe redirect validation logic
- Updated the `isSafeRedirect` function to improve validation of redirect URLs.
- Ensured that only safe relative paths are accepted, specifically excluding paths that lead to the login page.
- Refactored the logic to streamline the checks for valid redirect targets.
* test: add unit tests for redirect utility functions
- Introduced comprehensive tests for `isSafeRedirect`, `buildLoginRedirectUrl`, `getPostLoginRedirect`, and `persistRedirectToSession` functions.
- Validated various scenarios including safe and unsafe redirects, URL encoding, and session storage behavior.
- Enhanced test coverage to ensure robust handling of redirect logic and prevent potential security issues.
* chore: streamline authentication and redirect handling
- Removed unused `useLocation` import from `AuthContextProvider` and replaced its usage with `window.location` for better clarity.
- Updated `StartupLayout` to check for pending redirects before navigating to the new chat page, ensuring users are directed appropriately based on their session state.
- Enhanced unit tests for `useAuthRedirect` to verify correct handling of redirect parameters, including encoding of the current path and query parameters.
* test: add unit tests for StartupLayout redirect behavior
- Introduced a new test suite for the StartupLayout component to validate redirect logic based on authentication status and session storage.
- Implemented tests to ensure correct navigation to the new conversation page when authenticated without pending redirects, and to prevent navigation when a redirect URL parameter or session storage redirect is present.
- Enhanced coverage for scenarios where users are not authenticated, ensuring robust handling of redirect conditions.
---------
Co-authored-by: Vamsi Konakanchi <vamsi.k@trackmind.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: store hide_sequential_outputs before processStream clears config
processStream now clears config.configurable after completion to break
memory retention chains. Save hide_sequential_outputs to a local
variable before calling runAgents so the post-stream filter still works.
* feat: memory diagnostics
* chore: expose garbage collection in backend inspect command
Updated the backend inspect command in package.json to include the --expose-gc flag, enabling garbage collection diagnostics for improved memory management during development.
* chore: update @librechat/agents dependency to version 3.1.52
Bumped the version of @librechat/agents in package.json and package-lock.json to ensure compatibility and access to the latest features and fixes.
* fix: clear heavy config state after processStream to prevent memory leaks
Break the reference chain from LangGraph's internal __pregel_scratchpad
through @langchain/core RunTree.extra[lc:child_config] into the
AsyncLocalStorage context captured by timers and I/O handles.
After stream completion, null out symbol-keyed scratchpad properties
(currentTaskInput), config.configurable, and callbacks. Also call
Graph.clearHeavyState() to release config, signal, content maps,
handler registry, and tool sessions.
* chore: fix imports for memory utils
* chore: add circular dependency check in API build step
Enhanced the backend review workflow to include a check for circular dependencies during the API build process. If a circular dependency is detected, an error message is displayed, and the process exits with a failure status.
* chore: update API build step to include circular dependency detection
Modified the backend review workflow to rename the API package installation step to reflect its new functionality, which now includes detection of circular dependencies during the build process.
* chore: add memory diagnostics option to .env.example
Included a commented-out configuration option for enabling memory diagnostics in the .env.example file, which logs heap and RSS snapshots every 60 seconds when activated.
* chore: remove redundant agentContexts cleanup in disposeClient function
Streamlined the disposeClient function by eliminating duplicate cleanup logic for agentContexts, ensuring efficient memory management during client disposal.
* refactor: move runOutsideTracing utility to utils and update its usage
Refactored the runOutsideTracing function by relocating it to the utils module for better organization. Updated the tool execution handler to utilize the new import, ensuring consistent tracing behavior during tool execution.
* refactor: enhance connection management and diagnostics
Added a method to ConnectionsRepository for retrieving the active connection count. Updated UserConnectionManager to utilize this new method for app connection count reporting. Refined the OAuthReconnectionTracker's getStats method to improve clarity in diagnostics. Introduced a new tracing utility in the utils module to streamline tracing context management. Additionally, added a safeguard in memory diagnostics to prevent unnecessary snapshot collection for very short intervals.
* refactor: enhance tracing utility and add memory diagnostics tests
Refactored the runOutsideTracing function to improve warning logic when the AsyncLocalStorage context is missing. Added tests for memory diagnostics and tracing utilities to ensure proper functionality and error handling. Introduced a new test suite for memory diagnostics, covering snapshot collection and garbage collection behavior.
* 🔧 chore: Update `@eslint/eslintrc` and related dependencies in `package-lock.json` and `package.json` to latest versions for improved stability and performance
* 🔧 chore: Update `postcss-preset-env` to version 11.2.0 in `package-lock.json` and `client/package.json`, and add `eslint` dependency in `package.json` for improved linting support
* fix: Separate MCP GET SSE body timeout from POST and suppress SDK-internal stream recovery
- Add a dedicated GET Agent with a configurable `sseReadTimeout` (default 5 min,
matching the Python MCP SDK) so idle SSE streams time out independently of POST
requests, preventing the reconnect-loop log flood described in Discussion #11230.
- Suppress "SSE stream disconnected" and "Failed to reconnect SSE stream" errors
in setupTransportErrorHandlers — these are SDK-internal recovery events, not
transport failures. "Maximum reconnection attempts exceeded" still escalates.
- Add optional `sseReadTimeout` to BaseOptionsSchema for per-server configuration.
- Add 6 tests: agent timeout separation, custom sseReadTimeout, SSE disconnect
suppression (3 unit), and a real-server integration test proving the GET stream
recovers without a full transport rebuild.
* fix: Refactor MCP connection timeouts and error handling
- Updated the `DEFAULT_SSE_READ_TIMEOUT` to use a constant for better readability.
- Introduced internal error message constants for SSE stream disconnection and reconnection failures to improve maintainability.
- Enhanced type safety in tests by ensuring the options symbol is defined before usage.
- Updated the `sseReadTimeout` in `BaseOptionsSchema` to enforce positive values, ensuring valid configurations.
* chore: Update SSE read timeout documentation format in BaseOptionsSchema
- Changed the default timeout value comment in BaseOptionsSchema to use an underscore for better readability, aligning with common formatting practices.
* fix(a11y): hide collapsed thinking content from screen readers and link toggle to controlled region
The thinking/reasoning toggle button visually collapsed content using a CSS
grid animation (gridTemplateRows: 0fr + overflow-hidden), but the content
remained in the DOM and fully accessible to screen readers, cluttering the
reading flow for assistive technology users.
- Add aria-hidden={!isExpanded} to the collapsible content region in both
the legacy Thinking component and the modern Reasoning component, so
screen readers skip collapsed thoughts entirely
- Add role="region" and a unique id (via useId) to each collapsible content
div, giving it a semantic landmark for assistive technology
- Add contentId prop to the shared ThinkingButton and wire it to
aria-controls on the toggle button, establishing an explicit relationship
between the button and the region it expands/collapses
- aria-expanded was already present on the button; combined with
aria-controls, screen readers can now fully convey the toggle state and
its target
* fix(a11y): add aria-label to collapsible content regions in Thinking and Reasoning components
Enhanced accessibility by adding aria-label attributes to the collapsible content regions in both the Thinking and Reasoning components. This change ensures that screen readers can provide better context for users navigating through the content.
* fix(a11y): update roles and aria attributes in Thinking and Reasoning components
Changed role from "region" to "group" for collapsible content areas in both Thinking and Reasoning components to better align with ARIA practices. Updated aria-hidden to handle undefined values correctly and ensured contentId is passed to relevant components for improved accessibility and screen reader support.
* fix: error handling for transient HTTP request failures in MCP connection
- Added specific handling for the "fetch failed" TypeError, indicating that the request was aborted likely due to a timeout, while the connection remains usable.
- Updated the error message to provide clearer context for users regarding the transient nature of the error.
* refactor: MCPConnection with Agent Lifecycle Management
- Introduced an array to manage undici Agents, ensuring they are reused across requests and properly closed during disconnection.
- Updated the custom fetch and SSE connection methods to utilize the new Agent management system.
- Implemented error handling for SSE 404 responses based on session presence, improving connection stability.
- Added integration tests to validate the Agent lifecycle, ensuring agents are reused and closed correctly.
* fix: enhance error handling and connection management in MCPConnection
- Updated SSE connection timeout handling to use nullish coalescing for better defaulting.
- Improved the connection closure process by ensuring agents are properly closed and errors are logged non-fatally.
- Added tests to validate handling of "fetch failed" errors, marking them as transient and providing clearer messaging for users.
* fix: update timeout handling in MCPConnection for improved defaulting
- Changed timeout handling in MCPConnection to use logical OR instead of nullish coalescing for better default value assignment.
- Ensured consistent timeout behavior for both standard and SSE connections, enhancing reliability in connection management.
* 🔗 refactor: Replace navigate with Link for new chat navigation
* 🧬 fix: Ensure default action is prevented for non-left mouse clicks in NewChat component
* chore: saveToCloudStorage function and enhance error handling
- Removed unnecessary parameters and streamlined the logic for saving images to cloud storage.
- Introduced buffer handling for base64 image data and improved the integration with file strategy functions.
- Enhanced error handling during local image saving to ensure robustness.
- Updated the createGeminiImageTool function to reflect changes in the saveToCloudStorage implementation.
* refactor: streamline image persistence logic in GeminiImageGen
- Consolidated image saving functionality by renaming and refactoring the saveToCloudStorage function to persistGeneratedImage.
- Improved error handling and logging for image persistence operations.
- Enhanced the replaceUnwantedChars function to better sanitize input strings.
- Updated createGeminiImageTool to reflect changes in image handling and ensure consistent behavior across storage strategies.
* fix: clean up GeminiImageGen by removing unused functions and improving logging
- Removed the getSafeFormat and persistGeneratedImage functions to streamline image handling.
- Updated logging in createGeminiImageTool for clarity and consistency.
- Consolidated imports by eliminating unused dependencies, enhancing code maintainability.
* chore: update environment configuration and manifest for unused GEMINI_VERTEX_ENABLED
- Removed the Vertex AI configuration option from .env.example to simplify setup.
- Updated the manifest.json to reflect the removal of the Vertex AI dependency in the authentication field.
- Cleaned up the createGeminiImageTool function by eliminating unused fields related to Vertex AI, streamlining the code.
* fix: update loadAuthValues call in loadTools function for GeminiImageGen tool
- Modified the loadAuthValues function call to include throwError: false, preventing exceptions on authentication failures.
- Removed the unused processFileURL parameter from the tool context object, streamlining the code.
* refactor: streamline GoogleGenAI initialization in GeminiImageGen
- Removed unused file system access check for Google application credentials, simplifying the environment setup.
- Added googleAuthOptions to the GoogleGenAI instantiation, enhancing the configuration for authentication.
* fix: update Gemini API Key label and description in manifest.json
- Changed the label to indicate that the Gemini API Key is optional.
- Revised the description to clarify usage with Vertex AI and service accounts, enhancing user guidance.
* fix: enhance abort signal handling in createGeminiImageTool
- Introduced derivedSignal to manage abort events during image generation, improving responsiveness to cancellation requests.
- Added an abortHandler to log when image generation is aborted, enhancing debugging capabilities.
- Ensured proper cleanup of event listeners in the finally block to prevent memory leaks.
* fix: update authentication handling for plugins to support optional fields
- Added support for optional authentication fields in the manifest and PluginAuthForm.
- Updated the checkPluginAuth function to correctly validate plugins with optional fields.
- Enhanced tests to cover scenarios with optional authentication fields, ensuring accurate validation logic.
* feat: add aws bedrock upload to provider support
* chore: address copilot comments
* feat: add shared Bedrock document format types and MIME mapping
Bedrock Converse API accepts 9 document formats beyond PDF. Add
BedrockDocumentFormat union type, MIME-to-format mapping, and helpers
in data-provider so both client and backend can reference them.
* refactor: generalize Bedrock PDF validation to support all document types
Rename validateBedrockPdf to validateBedrockDocument with MIME-aware
logic: 4.5MB hard limit applies to all types, PDF header check only
runs for application/pdf. Adds test coverage for non-PDF documents.
* feat: support all Bedrock document formats in encoding pipeline
Widen file type gates to accept csv, doc, docx, xls, xlsx, html, txt,
md for Bedrock. Uses shared MIME-to-format map instead of hardcoded
'pdf'. Other providers' PDF-only paths remain unchanged.
* feat: expand Bedrock file upload UI to accept all document types
Add 'image_document_extended' upload type for Bedrock with accept
filters for all 9 supported formats. Update drag-and-drop validation
to use isBedrockDocumentType helper.
* fix: route Bedrock document types through provider pipeline
* fix: serve cached presigned URLs on agent list cache hits
On a cache hit the list endpoint was skipping the S3 refresh and
returning whatever presigned URL was stored in MongoDB, which could be
expired if the S3 URL TTL is shorter than the 30-minute cache window.
refreshListAvatars now collects a urlCache map (agentId -> refreshed
filepath) alongside its existing stats. The controller stores this map
in the cache instead of a plain boolean and re-applies it to every
paginated response, guaranteeing clients always receive a URL that was
valid as of the last refresh rather than a potentially stale DB value.
* fix: improve avatar refresh cache handling and logging
Updated the avatar refresh logic to validate cached refresh data before proceeding with S3 URL updates. Enhanced logging to exclude sensitive `urlCache` details while still providing relevant statistics. Added error handling for cache invalidation during avatar updates to ensure robustness.
* fix: update avatar refresh logic to clear urlCache on no change
Modified the avatar refresh function to clear the urlCache when no new path is generated, ensuring that stale URLs are not retained. This change improves cache handling and aligns with the updated logic for avatar updates.
* fix: enhance avatar refresh logic to handle legacy cache entries
Updated the avatar refresh logic to accommodate legacy boolean cache entries, ensuring they are treated as cache misses and triggering a refresh. The cache now stores a structured `urlCache` map instead of a boolean, improving cache handling. Added tests to verify correct behavior for cache hits and misses, ensuring clients receive valid URLs based on the latest refresh.
* feat: Added "document parser" OCR strategy
The document parser uses libraries to parse the text out of known document types.
This lets LibreChat handle some complex document types without having to use a
secondary service (like Mistral or standing up a RAG API server).
To enable the document parser, set the ocr strategy to "document_parser" in
librechat.yaml.
We now support:
- PDFs using pdfjs
- DOCX using mammoth
- XLS/XLSX using SheetJS
(The associated packages were also added to the project.)
* fix: applied Copilot code review suggestions
- Properly calculate length of text based on UTF8.
- Avoid issues with loading / blocking PDF parsing.
* fix: improved docs on parseDocument()
* chore: move to packages/api for TS support
* refactor: make document processing the default ocr strategy
- Introduced support for additional document types in the OCR strategy, including PDF, DOCX, and XLS/XLSX.
- Updated the file upload handling to dynamically select the appropriate parsing strategy based on the file type.
- Refactored the document parsing functions to use asynchronous imports for improved performance and maintainability.
* test: add unit tests for processAgentFileUpload functionality
- Introduced a new test suite for the processAgentFileUpload function in process.spec.js.
- Implemented various test cases to validate OCR strategy selection based on file types, including PDF, DOCX, XLSX, and XLS.
- Mocked dependencies to ensure isolated testing of file upload handling and strategy selection logic.
- Enhanced coverage for scenarios involving OCR capability checks and default strategy fallbacks.
* chore: update pdfjs-dist version and enhance document parsing tests
- Bumped pdfjs-dist dependency to version 5.4.624 in both api and packages/api.
- Refactored document parsing tests to use 'originalname' instead of 'filename' for file objects.
- Added a new test case for parsing XLS files to improve coverage of document types supported by the parser.
- Introduced a sample XLS file for testing purposes.
* feat: enforce text size limit and improve OCR fallback handling in processAgentFileUpload
- Added a check to ensure extracted text does not exceed the 15MB storage limit, throwing an error if it does.
- Refactored the OCR handling logic to improve fallback behavior when the configured OCR fails, ensuring a more robust document processing flow.
- Enhanced unit tests to cover scenarios for oversized text and fallback mechanisms, ensuring proper error handling and functionality.
* fix: correct OCR URL construction in performOCR function
- Updated the OCR URL construction to ensure it correctly appends '/ocr' to the base URL if not already present, improving the reliability of the OCR request.
---------
Co-authored-by: Dan Lew <daniel@mightyacorn.com>
@@ -38,7 +38,7 @@ Project maintainers have the right and responsibility to remove, edit, or reject
- Install [MongoDB Community Edition](https://www.mongodb.com/docs/manual/administration/install-community/), ensure that `mongosh` connects to your local instance.
- Run: `npx install playwright`, then `npx playwright install`.
<textx="378"y="322"font-family="Inter,system-ui,sans-serif"font-size="30"fill="#ffffff"opacity=".66">AI chat with MCP integration and multi-provider support</text>
# this workflow is triggered by an API call when there is a new PyPI release of LiteLLM
name:Build, Publish LiteLLM Docker Image. New Release
on:
workflow_dispatch:
inputs:
tag:
description:"The tag version you want to build"
release_type:
description:"The release type you want to build. Can be 'latest', 'stable', 'dev', 'rc'"
type:string
default:"latest"
commit_hash:
description:"Commit hash"
required:true
# Defines two custom environment variables for the workflow. Used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY:ghcr.io
IMAGE_NAME:${{ github.repository }}
CHART_NAME:litellm-helm
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents:read
packages:write
steps:
- name:Checkout repository
uses:actions/checkout@v6
with:
ref:${{ github.event.inputs.commit_hash }}
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
# this workflow is triggered by an API call when there is a new PyPI release of LiteLLM
name:Build, Publish LiteLLM Helm Chart. New Release
on:
workflow_dispatch:
inputs:
chartVersion:
description:"Update the helm chart's version to this"
# Defines two custom environment variables for the workflow. Used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY:ghcr.io
IMAGE_NAME:${{ github.repository }}
REPO_OWNER:${{github.repository_owner}}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
The `@librechat/agents` package is a major backend dependency (internal npm package, name kept for compatibility).
---
## Workspace Boundaries
- **All new backend code must be TypeScript** in `/packages/api`.
- Keep `/api` changes to the absolute minimum (thin JS wrappers calling into `/packages/api`).
- Database-specific shared logic goes in `/packages/data-schemas`.
- Frontend/backend shared API logic (endpoints, types, data-service) goes in `/packages/data-provider`.
- Build data-provider from project root: `npm run build:data-provider`.
---
## Code Style
### Structure and Clarity
- **Never-nesting**: early returns, flat code, minimal indentation. Break complex operations into well-named helpers.
- **Functional first**: pure functions, immutable data, `map`/`filter`/`reduce` over imperative loops. Only reach for OOP when it clearly improves domain modeling or state encapsulation.
- Reusable hooks / higher-order components for UI patterns.
- Parameterized helpers instead of near-duplicate functions.
- Constants for repeated values; configuration objects over duplicated init code.
- Shared validators, centralized error handling, single source of truth for business rules.
- Shared typing system with interfaces/types extending common base definitions.
- Abstraction layers for external API interactions.
### Iteration and Performance
- **Minimize looping** — especially over shared data structures like message arrays, which are iterated frequently throughout the codebase. Every additional pass adds up at scale.
- Consolidate sequential O(n) operations into a single pass whenever possible; never loop over the same collection twice if the work can be combined.
- Choose data structures that reduce the need to iterate (e.g., `Map`/`Set` for lookups instead of `Array.find`/`Array.includes`).
- Prevent memory leaks: careful with closures, dispose resources/event listeners, no circular references.
### Type Safety
- **Never use `any`**. Explicit types for all parameters, return values, and variables.
- **Limit `unknown`** — avoid `unknown`, `Record<string, unknown>`, and `as unknown as T` assertions. A `Record<string, unknown>` almost always signals a missing explicit type definition.
- **Don't duplicate types** — before defining a new type, check whether it already exists in the project (especially `packages/data-provider`). Reuse and extend existing types rather than creating redundant definitions.
- Use union types, generics, and interfaces appropriately.
- All TypeScript and ESLint warnings/errors must be addressed — do not leave unresolved diagnostics.
### Comments and Documentation
- Write self-documenting code; no inline comments narrating what code does.
- JSDoc only for complex/non-obvious logic or intellisense on public APIs.
- Single-line JSDoc for brief docs, multi-line for complex cases.
1.**Package imports** — sorted shortest to longest line length (`react` always first).
2.**`import type` imports** — sorted longest to shortest (package types first, then local types; length resets between sub-groups).
3.**Local/project imports** — sorted longest to shortest.
Multi-line imports count total character length across all lines. Consolidate value imports from the same module. Always use standalone `import type { ... }` — never inline `type` inside value imports.
### JS/TS Loop Preferences
- **Limit looping as much as possible.** Prefer single-pass transformations and avoid re-iterating the same data.
-`for (let i = 0; ...)` for performance-critical or index-dependent operations.
-`for...of` for simple array iteration.
-`for...in` only for object property enumeration.
---
## Frontend Rules (`client/src/**/*`)
### Localization
- All user-facing text must use `useLocalize()`.
- Only update English keys in `client/src/locales/en/translation.json` (other languages are automated externally).
- Semantic key prefixes: `com_ui_`, `com_assistants_`, etc.
### Components
- TypeScript for all React components with proper type imports.
- Semantic HTML with ARIA labels (`role`, `aria-label`) for accessibility.
- Group related components in feature directories (e.g., `SidePanel/Memories/`).
- Use `encodeURIComponent` for dynamic URL parameters.
### Performance
- Prioritize memory and speed efficiency at scale.
- Cursor pagination for large datasets.
- Proper dependency arrays to avoid unnecessary re-renders.
- Leverage React Query caching and background refetching.
---
## Development Commands
| Command | Purpose |
|---|---|
| `npm run smart-reinstall` | Install deps (if lockfile changed) + build via Turborepo |
| `npm run reinstall` | Clean install — wipe `node_modules` and reinstall from scratch |
| `npm run backend` | Start the backend server |
| `npm run backend:dev` | Start backend with file watching (development) |
| `npm run build` | Build all compiled code via Turborepo (parallel, cached) |
| `npm run frontend` | Build all compiled code sequentially (legacy fallback) |
| `npm run frontend:dev` | Start frontend dev server with HMR (port 3090, requires backend running) |
| `npm run build:data-provider` | Rebuild `packages/data-provider` after changes |
- Node.js: v20.19.0+ or ^22.12.0 or >= 23.0.0
- Database: MongoDB
- Backend runs on `http://localhost:3080/`; frontend dev server on `http://localhost:3090/`
---
## Testing
- Framework: **Jest**, run per-workspace.
- Run tests from their workspace directory: `cd api && npx jest <pattern>`, `cd packages/api && npx jest <pattern>`, etc.
- Frontend tests: `__tests__` directories alongside components; use `test/layout-test-utils` for rendering.
- Cover loading, success, and error states for UI/data flows.
- Mock data-provider hooks and external dependencies.
---
## Formatting
Fix all formatting lint errors (trailing spaces, tabs, newlines, indentation) using auto-fix when available. All TypeScript/ESLint warnings and errors **must** be resolved.
@@ -11,41 +11,41 @@ All notable changes to this project will be documented in this file.
### ✨ New Features
- ✨ feat: implement search parameter updates by **@mawburn** in [#7151](https://github.com/danny-avila/LibreChat/pull/7151)
- 🎏 feat: Add MCP support for Streamable HTTP Transport by **@benverhees** in [#7353](https://github.com/danny-avila/LibreChat/pull/7353)
- 🔒 feat: Add Content Security Policy using Helmet middleware by **@rubentalstra** in [#7377](https://github.com/danny-avila/LibreChat/pull/7377)
- ✨ feat: Add Normalization for MCP Server Names by **@danny-avila** in [#7421](https://github.com/danny-avila/LibreChat/pull/7421)
- 📊 feat: Improve Helm Chart by **@hofq** in [#3638](https://github.com/danny-avila/LibreChat/pull/3638)
- 🦾 feat: Claude-4 Support by **@danny-avila** in [#7509](https://github.com/danny-avila/LibreChat/pull/7509)
- 🪨 feat: Bedrock Support for Claude-4 Reasoning by **@danny-avila** in [#7517](https://github.com/danny-avila/LibreChat/pull/7517)
- ✨ feat: implement search parameter updates by **@mawburn** in [#7151](https://github.com/danny-avila/Chat/pull/7151)
- 🎏 feat: Add MCP support for Streamable HTTP Transport by **@benverhees** in [#7353](https://github.com/danny-avila/Chat/pull/7353)
- 🔒 feat: Add Content Security Policy using Helmet middleware by **@rubentalstra** in [#7377](https://github.com/danny-avila/Chat/pull/7377)
- ✨ feat: Add Normalization for MCP Server Names by **@danny-avila** in [#7421](https://github.com/danny-avila/Chat/pull/7421)
- 📊 feat: Improve Helm Chart by **@hofq** in [#3638](https://github.com/danny-avila/Chat/pull/3638)
- 🦾 feat: Claude-4 Support by **@danny-avila** in [#7509](https://github.com/danny-avila/Chat/pull/7509)
- 🪨 feat: Bedrock Support for Claude-4 Reasoning by **@danny-avila** in [#7517](https://github.com/danny-avila/Chat/pull/7517)
### 🌍 Internationalization
- 🌍 i18n: Add `Danish` and `Czech` and `Catalan` localization support by **@rubentalstra** in [#7373](https://github.com/danny-avila/LibreChat/pull/7373)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7375](https://github.com/danny-avila/LibreChat/pull/7375)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7468](https://github.com/danny-avila/LibreChat/pull/7468)
- 🌍 i18n: Add `Danish` and `Czech` and `Catalan` localization support by **@rubentalstra** in [#7373](https://github.com/danny-avila/Chat/pull/7373)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7375](https://github.com/danny-avila/Chat/pull/7375)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7468](https://github.com/danny-avila/Chat/pull/7468)
### 🔧 Fixes
- 💬 fix: update aria-label for accessibility in ConvoLink component by **@berry-13** in [#7320](https://github.com/danny-avila/LibreChat/pull/7320)
- 🔑 fix: use `apiKey` instead of `openAIApiKey` in OpenAI-like Config by **@danny-avila** in [#7337](https://github.com/danny-avila/LibreChat/pull/7337)
- 🔄 fix: update navigation logic in `useFocusChatEffect` to ensure correct search parameters are used by **@mawburn** in [#7340](https://github.com/danny-avila/LibreChat/pull/7340)
- 🔄 fix: Improve MCP Connection Cleanup by **@danny-avila** in [#7400](https://github.com/danny-avila/LibreChat/pull/7400)
- 🛡️ fix: Preset and Validation Logic for URL Query Params by **@danny-avila** in [#7407](https://github.com/danny-avila/LibreChat/pull/7407)
- 🌘 fix: artifact of preview text is illegible in dark mode by **@nhtruong** in [#7405](https://github.com/danny-avila/LibreChat/pull/7405)
- 🛡️ fix: Temporarily Remove CSP until Configurable by **@danny-avila** in [#7419](https://github.com/danny-avila/LibreChat/pull/7419)
- 💽 fix: Exclude index page `/` from static cache settings by **@sbruel** in [#7382](https://github.com/danny-avila/LibreChat/pull/7382)
- 💬 fix: update aria-label for accessibility in ConvoLink component by **@berry-13** in [#7320](https://github.com/danny-avila/Chat/pull/7320)
- 🔑 fix: use `apiKey` instead of `openAIApiKey` in OpenAI-like Config by **@danny-avila** in [#7337](https://github.com/danny-avila/Chat/pull/7337)
- 🔄 fix: update navigation logic in `useFocusChatEffect` to ensure correct search parameters are used by **@mawburn** in [#7340](https://github.com/danny-avila/Chat/pull/7340)
- 🔄 fix: Improve MCP Connection Cleanup by **@danny-avila** in [#7400](https://github.com/danny-avila/Chat/pull/7400)
- 🛡️ fix: Preset and Validation Logic for URL Query Params by **@danny-avila** in [#7407](https://github.com/danny-avila/Chat/pull/7407)
- 🌘 fix: artifact of preview text is illegible in dark mode by **@nhtruong** in [#7405](https://github.com/danny-avila/Chat/pull/7405)
- 🛡️ fix: Temporarily Remove CSP until Configurable by **@danny-avila** in [#7419](https://github.com/danny-avila/Chat/pull/7419)
- 💽 fix: Exclude index page `/` from static cache settings by **@sbruel** in [#7382](https://github.com/danny-avila/Chat/pull/7382)
### ⚙️ Other Changes
- 📜 docs: CHANGELOG for release v0.7.8 by **@github-actions[bot]** in [#7290](https://github.com/danny-avila/LibreChat/pull/7290)
- 📦 chore: Update API Package Dependencies by **@danny-avila** in [#7359](https://github.com/danny-avila/LibreChat/pull/7359)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7321](https://github.com/danny-avila/LibreChat/pull/7321)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7434](https://github.com/danny-avila/LibreChat/pull/7434)
- 🛡️ chore: `multer` v2.0.0 for CVE-2025-47935 and CVE-2025-47944 by **@danny-avila** in [#7454](https://github.com/danny-avila/LibreChat/pull/7454)
- 📂 refactor: Improve `FileAttachment` & File Form Deletion by **@danny-avila** in [#7471](https://github.com/danny-avila/LibreChat/pull/7471)
- 📊 chore: Remove Old Helm Chart by **@hofq** in [#7512](https://github.com/danny-avila/LibreChat/pull/7512)
- 🪖 chore: bump helm app version to v0.7.8 by **@austin-barrington** in [#7524](https://github.com/danny-avila/LibreChat/pull/7524)
- 📜 docs: CHANGELOG for release v0.7.8 by **@github-actions[bot]** in [#7290](https://github.com/danny-avila/Chat/pull/7290)
- 📦 chore: Update API Package Dependencies by **@danny-avila** in [#7359](https://github.com/danny-avila/Chat/pull/7359)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7321](https://github.com/danny-avila/Chat/pull/7321)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7434](https://github.com/danny-avila/Chat/pull/7434)
- 🛡️ chore: `multer` v2.0.0 for CVE-2025-47935 and CVE-2025-47944 by **@danny-avila** in [#7454](https://github.com/danny-avila/Chat/pull/7454)
- 📂 refactor: Improve `FileAttachment` & File Form Deletion by **@danny-avila** in [#7471](https://github.com/danny-avila/Chat/pull/7471)
- 📊 chore: Remove Old Helm Chart by **@hofq** in [#7512](https://github.com/danny-avila/Chat/pull/7512)
- 🪖 chore: bump helm app version to v0.7.8 by **@austin-barrington** in [#7524](https://github.com/danny-avila/Chat/pull/7524)
@@ -56,38 +56,38 @@ Changes from v0.7.8-rc1 to v0.7.8.
### ✨ New Features
- ✨ feat: Enhance form submission for touch screens by **@berry-13** in [#7198](https://github.com/danny-avila/LibreChat/pull/7198)
- 🔍 feat: Additional Tavily API Tool Parameters by **@glowforge-opensource** in [#7232](https://github.com/danny-avila/LibreChat/pull/7232)
- 🐋 feat: Add python to Dockerfile for increased MCP compatibility by **@technicalpickles** in [#7270](https://github.com/danny-avila/LibreChat/pull/7270)
- ✨ feat: Enhance form submission for touch screens by **@berry-13** in [#7198](https://github.com/danny-avila/Chat/pull/7198)
- 🔍 feat: Additional Tavily API Tool Parameters by **@glowforge-opensource** in [#7232](https://github.com/danny-avila/Chat/pull/7232)
- 🐋 feat: Add python to Dockerfile for increased MCP compatibility by **@technicalpickles** in [#7270](https://github.com/danny-avila/Chat/pull/7270)
### 🔧 Fixes
- 🔧 fix: Google Gemma Support & OpenAI Reasoning Instructions by **@danny-avila** in [#7196](https://github.com/danny-avila/LibreChat/pull/7196)
- 🛠️ fix: Conversation Navigation State by **@danny-avila** in [#7210](https://github.com/danny-avila/LibreChat/pull/7210)
- 🔄 fix: o-Series Model Regex for System Messages by **@danny-avila** in [#7245](https://github.com/danny-avila/LibreChat/pull/7245)
- 🔖 fix: Custom Headers for Initial MCP SSE Connection by **@danny-avila** in [#7246](https://github.com/danny-avila/LibreChat/pull/7246)
- 🛡️ fix: Deep Clone `MCPOptions` for User MCP Connections by **@danny-avila** in [#7247](https://github.com/danny-avila/LibreChat/pull/7247)
- 🔄 fix: URL Param Race Condition and File Draft Persistence by **@danny-avila** in [#7257](https://github.com/danny-avila/LibreChat/pull/7257)
- 🔄 fix: Assistants Endpoint & Minor Issues by **@danny-avila** in [#7274](https://github.com/danny-avila/LibreChat/pull/7274)
- 🔄 fix: Ollama Think Tag Edge Case with Tools by **@danny-avila** in [#7275](https://github.com/danny-avila/LibreChat/pull/7275)
- 🔧 fix: Google Gemma Support & OpenAI Reasoning Instructions by **@danny-avila** in [#7196](https://github.com/danny-avila/Chat/pull/7196)
- 🛠️ fix: Conversation Navigation State by **@danny-avila** in [#7210](https://github.com/danny-avila/Chat/pull/7210)
- 🔄 fix: o-Series Model Regex for System Messages by **@danny-avila** in [#7245](https://github.com/danny-avila/Chat/pull/7245)
- 🔖 fix: Custom Headers for Initial MCP SSE Connection by **@danny-avila** in [#7246](https://github.com/danny-avila/Chat/pull/7246)
- 🛡️ fix: Deep Clone `MCPOptions` for User MCP Connections by **@danny-avila** in [#7247](https://github.com/danny-avila/Chat/pull/7247)
- 🔄 fix: URL Param Race Condition and File Draft Persistence by **@danny-avila** in [#7257](https://github.com/danny-avila/Chat/pull/7257)
- 🔄 fix: Assistants Endpoint & Minor Issues by **@danny-avila** in [#7274](https://github.com/danny-avila/Chat/pull/7274)
- 🔄 fix: Ollama Think Tag Edge Case with Tools by **@danny-avila** in [#7275](https://github.com/danny-avila/Chat/pull/7275)
### ⚙️ Other Changes
- 📜 docs: CHANGELOG for release v0.7.8-rc1 by **@github-actions[bot]** in [#7153](https://github.com/danny-avila/LibreChat/pull/7153)
- 🔄 refactor: Artifact Visibility Management by **@danny-avila** in [#7181](https://github.com/danny-avila/LibreChat/pull/7181)
- 📦 chore: Bump Package Security by **@danny-avila** in [#7183](https://github.com/danny-avila/LibreChat/pull/7183)
- 🌿 refactor: Unmount Fork Popover on Hide for Better Performance by **@danny-avila** in [#7189](https://github.com/danny-avila/LibreChat/pull/7189)
- 🧰 chore: ESLint configuration to enforce Prettier formatting rules by **@mawburn** in [#7186](https://github.com/danny-avila/LibreChat/pull/7186)
- 🎨 style: Improve KaTeX Rendering for LaTeX Equations by **@andresgit** in [#7223](https://github.com/danny-avila/LibreChat/pull/7223)
- 📝 docs: Update `.env.example` Google models by **@marlonka** in [#7254](https://github.com/danny-avila/LibreChat/pull/7254)
- 💬 refactor: MCP Chat Visibility Option, Google Rates, Remove OpenAPI Plugins by **@danny-avila** in [#7286](https://github.com/danny-avila/LibreChat/pull/7286)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7214](https://github.com/danny-avila/LibreChat/pull/7214)
- 📜 docs: CHANGELOG for release v0.7.8-rc1 by **@github-actions[bot]** in [#7153](https://github.com/danny-avila/Chat/pull/7153)
- 🔄 refactor: Artifact Visibility Management by **@danny-avila** in [#7181](https://github.com/danny-avila/Chat/pull/7181)
- 📦 chore: Bump Package Security by **@danny-avila** in [#7183](https://github.com/danny-avila/Chat/pull/7183)
- 🌿 refactor: Unmount Fork Popover on Hide for Better Performance by **@danny-avila** in [#7189](https://github.com/danny-avila/Chat/pull/7189)
- 🧰 chore: ESLint configuration to enforce Prettier formatting rules by **@mawburn** in [#7186](https://github.com/danny-avila/Chat/pull/7186)
- 🎨 style: Improve KaTeX Rendering for LaTeX Equations by **@andresgit** in [#7223](https://github.com/danny-avila/Chat/pull/7223)
- 📝 docs: Update `.env.example` Google models by **@marlonka** in [#7254](https://github.com/danny-avila/Chat/pull/7254)
- 💬 refactor: MCP Chat Visibility Option, Google Rates, Remove OpenAPI Plugins by **@danny-avila** in [#7286](https://github.com/danny-avila/Chat/pull/7286)
- 📜 docs: Unreleased Changelog by **@github-actions[bot]** in [#7214](https://github.com/danny-avila/Chat/pull/7214)
@@ -96,141 +96,141 @@ Changes from v0.7.7 to v0.7.8-rc1.
### ✨ New Features
- 🔍 feat: Mistral OCR API / Upload Files as Text by **@danny-avila** in [#6274](https://github.com/danny-avila/LibreChat/pull/6274)
- 🤖 feat: Support OpenAI Web Search models by **@danny-avila** in [#6313](https://github.com/danny-avila/LibreChat/pull/6313)
- 🔗 feat: Agent Chain (Mixture-of-Agents) by **@danny-avila** in [#6374](https://github.com/danny-avila/LibreChat/pull/6374)
- ⌛ feat: `initTimeout` for Slow Starting MCP Servers by **@perweij** in [#6383](https://github.com/danny-avila/LibreChat/pull/6383)
- 🚀 feat: `S3` Integration for File handling and Image uploads by **@rubentalstra** in [#6142](https://github.com/danny-avila/LibreChat/pull/6142)
- 🔒feat: Enable OpenID Auto-Redirect by **@leondape** in [#6066](https://github.com/danny-avila/LibreChat/pull/6066)
- 🚀 feat: Integrate `Azure Blob Storage` for file handling and image uploads by **@rubentalstra** in [#6153](https://github.com/danny-avila/LibreChat/pull/6153)
- 🚀 feat: Add support for custom `AWS` endpoint in `S3` by **@rubentalstra** in [#6431](https://github.com/danny-avila/LibreChat/pull/6431)
- 🚀 feat: Add support for LDAP STARTTLS in LDAP authentication by **@rubentalstra** in [#6438](https://github.com/danny-avila/LibreChat/pull/6438)
- 🚀 feat: Refactor schema exports and update package version to 0.0.4 by **@rubentalstra** in [#6455](https://github.com/danny-avila/LibreChat/pull/6455)
- 🔼 feat: Add Auto Submit For URL Query Params by **@mjaverto** in [#6440](https://github.com/danny-avila/LibreChat/pull/6440)
- 🛠 feat: Enhance Redis Integration, Rate Limiters & Log Headers by **@danny-avila** in [#6462](https://github.com/danny-avila/LibreChat/pull/6462)
- 💵 feat: Add Automatic Balance Refill by **@rubentalstra** in [#6452](https://github.com/danny-avila/LibreChat/pull/6452)
- 🗣️ feat: add support for gpt-4o-transcribe models by **@berry-13** in [#6483](https://github.com/danny-avila/LibreChat/pull/6483)
- 🎨 feat: UI Refresh for Enhanced UX by **@berry-13** in [#6346](https://github.com/danny-avila/LibreChat/pull/6346)
- 🌍 feat: Add support for Hungarian language localization by **@rubentalstra** in [#6508](https://github.com/danny-avila/LibreChat/pull/6508)
- 🚀 feat: Add Gemini 2.5 Token/Context Values, Increase Max Possible Output to 64k by **@danny-avila** in [#6563](https://github.com/danny-avila/LibreChat/pull/6563)
- 🚀 feat: Enhance MCP Connections For Multi-User Support by **@danny-avila** in [#6610](https://github.com/danny-avila/LibreChat/pull/6610)
- 🚀 feat: Enhance S3 URL Expiry with Refresh; fix: S3 File Deletion by **@danny-avila** in [#6647](https://github.com/danny-avila/LibreChat/pull/6647)
- 🚀 feat: enhance UI components and refactor settings by **@berry-13** in [#6625](https://github.com/danny-avila/LibreChat/pull/6625)
- 💬 feat: move TemporaryChat to the Header by **@berry-13** in [#6646](https://github.com/danny-avila/LibreChat/pull/6646)
- 🚀 feat: Use Model Specs + Specific Endpoints, Limit Providers for Agents by **@danny-avila** in [#6650](https://github.com/danny-avila/LibreChat/pull/6650)
- 🪙 feat: Sync Balance Config on Login by **@danny-avila** in [#6671](https://github.com/danny-avila/LibreChat/pull/6671)
- 🔦 feat: MCP Support for Non-Agent Endpoints by **@danny-avila** in [#6775](https://github.com/danny-avila/LibreChat/pull/6775)
- 🗃️ feat: Code Interpreter File Persistence between Sessions by **@danny-avila** in [#6790](https://github.com/danny-avila/LibreChat/pull/6790)
- 🖥️ feat: Code Interpreter API for Non-Agent Endpoints by **@danny-avila** in [#6803](https://github.com/danny-avila/LibreChat/pull/6803)
- ⚡ feat: Self-hosted Artifacts Static Bundler URL by **@danny-avila** in [#6827](https://github.com/danny-avila/LibreChat/pull/6827)
- 🐳 feat: Add Jemalloc and UV to Docker Builds by **@danny-avila** in [#6836](https://github.com/danny-avila/LibreChat/pull/6836)
- 🤖 feat: GPT-4.1 by **@danny-avila** in [#6880](https://github.com/danny-avila/LibreChat/pull/6880)
- 👋 feat: remove Edge TTS by **@berry-13** in [#6885](https://github.com/danny-avila/LibreChat/pull/6885)
- feat: nav optimization by **@berry-13** in [#5785](https://github.com/danny-avila/LibreChat/pull/5785)
- 🗺️ feat: Add Parameter Location Mapping for OpenAPI actions by **@peeeteeer** in [#6858](https://github.com/danny-avila/LibreChat/pull/6858)
- 🤖 feat: Support `o4-mini` and `o3` Models by **@danny-avila** in [#6928](https://github.com/danny-avila/LibreChat/pull/6928)
- 🎨 feat: OpenAI Image Tools (GPT-Image-1) by **@danny-avila** in [#7079](https://github.com/danny-avila/LibreChat/pull/7079)
- 🗓️ feat: Add Special Variables for Prompts & Agents, Prompt UI Improvements by **@danny-avila** in [#7123](https://github.com/danny-avila/LibreChat/pull/7123)
- 🔍 feat: Mistral OCR API / Upload Files as Text by **@danny-avila** in [#6274](https://github.com/danny-avila/Chat/pull/6274)
- 🤖 feat: Support OpenAI Web Search models by **@danny-avila** in [#6313](https://github.com/danny-avila/Chat/pull/6313)
- 🔗 feat: Agent Chain (Mixture-of-Agents) by **@danny-avila** in [#6374](https://github.com/danny-avila/Chat/pull/6374)
- ⌛ feat: `initTimeout` for Slow Starting MCP Servers by **@perweij** in [#6383](https://github.com/danny-avila/Chat/pull/6383)
- 🚀 feat: `S3` Integration for File handling and Image uploads by **@rubentalstra** in [#6142](https://github.com/danny-avila/Chat/pull/6142)
- 🔒feat: Enable OpenID Auto-Redirect by **@leondape** in [#6066](https://github.com/danny-avila/Chat/pull/6066)
- 🚀 feat: Integrate `Azure Blob Storage` for file handling and image uploads by **@rubentalstra** in [#6153](https://github.com/danny-avila/Chat/pull/6153)
- 🚀 feat: Add support for custom `AWS` endpoint in `S3` by **@rubentalstra** in [#6431](https://github.com/danny-avila/Chat/pull/6431)
- 🚀 feat: Add support for LDAP STARTTLS in LDAP authentication by **@rubentalstra** in [#6438](https://github.com/danny-avila/Chat/pull/6438)
- 🚀 feat: Refactor schema exports and update package version to 0.0.4 by **@rubentalstra** in [#6455](https://github.com/danny-avila/Chat/pull/6455)
- 🔼 feat: Add Auto Submit For URL Query Params by **@mjaverto** in [#6440](https://github.com/danny-avila/Chat/pull/6440)
- 🛠 feat: Enhance Redis Integration, Rate Limiters & Log Headers by **@danny-avila** in [#6462](https://github.com/danny-avila/Chat/pull/6462)
- 💵 feat: Add Automatic Balance Refill by **@rubentalstra** in [#6452](https://github.com/danny-avila/Chat/pull/6452)
- 🗣️ feat: add support for gpt-4o-transcribe models by **@berry-13** in [#6483](https://github.com/danny-avila/Chat/pull/6483)
- 🎨 feat: UI Refresh for Enhanced UX by **@berry-13** in [#6346](https://github.com/danny-avila/Chat/pull/6346)
- 🌍 feat: Add support for Hungarian language localization by **@rubentalstra** in [#6508](https://github.com/danny-avila/Chat/pull/6508)
- 🚀 feat: Add Gemini 2.5 Token/Context Values, Increase Max Possible Output to 64k by **@danny-avila** in [#6563](https://github.com/danny-avila/Chat/pull/6563)
- 🚀 feat: Enhance MCP Connections For Multi-User Support by **@danny-avila** in [#6610](https://github.com/danny-avila/Chat/pull/6610)
- 🚀 feat: Enhance S3 URL Expiry with Refresh; fix: S3 File Deletion by **@danny-avila** in [#6647](https://github.com/danny-avila/Chat/pull/6647)
- 🚀 feat: enhance UI components and refactor settings by **@berry-13** in [#6625](https://github.com/danny-avila/Chat/pull/6625)
- 💬 feat: move TemporaryChat to the Header by **@berry-13** in [#6646](https://github.com/danny-avila/Chat/pull/6646)
- 🚀 feat: Use Model Specs + Specific Endpoints, Limit Providers for Agents by **@danny-avila** in [#6650](https://github.com/danny-avila/Chat/pull/6650)
- 🪙 feat: Sync Balance Config on Login by **@danny-avila** in [#6671](https://github.com/danny-avila/Chat/pull/6671)
- 🔦 feat: MCP Support for Non-Agent Endpoints by **@danny-avila** in [#6775](https://github.com/danny-avila/Chat/pull/6775)
- 🗃️ feat: Code Interpreter File Persistence between Sessions by **@danny-avila** in [#6790](https://github.com/danny-avila/Chat/pull/6790)
- 🖥️ feat: Code Interpreter API for Non-Agent Endpoints by **@danny-avila** in [#6803](https://github.com/danny-avila/Chat/pull/6803)
- ⚡ feat: Self-hosted Artifacts Static Bundler URL by **@danny-avila** in [#6827](https://github.com/danny-avila/Chat/pull/6827)
- 🐳 feat: Add Jemalloc and UV to Docker Builds by **@danny-avila** in [#6836](https://github.com/danny-avila/Chat/pull/6836)
- 🤖 feat: GPT-4.1 by **@danny-avila** in [#6880](https://github.com/danny-avila/Chat/pull/6880)
- 👋 feat: remove Edge TTS by **@berry-13** in [#6885](https://github.com/danny-avila/Chat/pull/6885)
- feat: nav optimization by **@berry-13** in [#5785](https://github.com/danny-avila/Chat/pull/5785)
- 🗺️ feat: Add Parameter Location Mapping for OpenAPI actions by **@peeeteeer** in [#6858](https://github.com/danny-avila/Chat/pull/6858)
- 🤖 feat: Support `o4-mini` and `o3` Models by **@danny-avila** in [#6928](https://github.com/danny-avila/Chat/pull/6928)
- 🎨 feat: OpenAI Image Tools (GPT-Image-1) by **@danny-avila** in [#7079](https://github.com/danny-avila/Chat/pull/7079)
- 🗓️ feat: Add Special Variables for Prompts & Agents, Prompt UI Improvements by **@danny-avila** in [#7123](https://github.com/danny-avila/Chat/pull/7123)
### 🌍 Internationalization
- 🌍 i18n: Add Thai Language Support and Update Translations by **@rubentalstra** in [#6219](https://github.com/danny-avila/LibreChat/pull/6219)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6220](https://github.com/danny-avila/LibreChat/pull/6220)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6240](https://github.com/danny-avila/LibreChat/pull/6240)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6241](https://github.com/danny-avila/LibreChat/pull/6241)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6277](https://github.com/danny-avila/LibreChat/pull/6277)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6414](https://github.com/danny-avila/LibreChat/pull/6414)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6505](https://github.com/danny-avila/LibreChat/pull/6505)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6530](https://github.com/danny-avila/LibreChat/pull/6530)
- 🌍 i18n: Add Persian Localization Support by **@rubentalstra** in [#6669](https://github.com/danny-avila/LibreChat/pull/6669)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6667](https://github.com/danny-avila/LibreChat/pull/6667)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7126](https://github.com/danny-avila/LibreChat/pull/7126)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7148](https://github.com/danny-avila/LibreChat/pull/7148)
- 🌍 i18n: Add Thai Language Support and Update Translations by **@rubentalstra** in [#6219](https://github.com/danny-avila/Chat/pull/6219)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6220](https://github.com/danny-avila/Chat/pull/6220)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6240](https://github.com/danny-avila/Chat/pull/6240)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6241](https://github.com/danny-avila/Chat/pull/6241)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6277](https://github.com/danny-avila/Chat/pull/6277)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6414](https://github.com/danny-avila/Chat/pull/6414)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6505](https://github.com/danny-avila/Chat/pull/6505)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6530](https://github.com/danny-avila/Chat/pull/6530)
- 🌍 i18n: Add Persian Localization Support by **@rubentalstra** in [#6669](https://github.com/danny-avila/Chat/pull/6669)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6667](https://github.com/danny-avila/Chat/pull/6667)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7126](https://github.com/danny-avila/Chat/pull/7126)
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7148](https://github.com/danny-avila/Chat/pull/7148)
### 👐 Accessibility
- 🎨 a11y: Update Model Spec Description Text by **@berry-13** in [#6294](https://github.com/danny-avila/LibreChat/pull/6294)
- 🗑️ a11y: Add Accessible Name to Button for File Attachment Removal by **@kangabell** in [#6709](https://github.com/danny-avila/LibreChat/pull/6709)
- ⌨️ a11y: enhance accessibility & visual consistency by **@berry-13** in [#6866](https://github.com/danny-avila/LibreChat/pull/6866)
- 🙌 a11y: Searchbar/Conversations List Focus by **@danny-avila** in [#7096](https://github.com/danny-avila/LibreChat/pull/7096)
- 👐 a11y: Improve Fork and SplitText Accessibility by **@danny-avila** in [#7147](https://github.com/danny-avila/LibreChat/pull/7147)
- 🎨 a11y: Update Model Spec Description Text by **@berry-13** in [#6294](https://github.com/danny-avila/Chat/pull/6294)
- 🗑️ a11y: Add Accessible Name to Button for File Attachment Removal by **@kangabell** in [#6709](https://github.com/danny-avila/Chat/pull/6709)
- ⌨️ a11y: enhance accessibility & visual consistency by **@berry-13** in [#6866](https://github.com/danny-avila/Chat/pull/6866)
- 🙌 a11y: Searchbar/Conversations List Focus by **@danny-avila** in [#7096](https://github.com/danny-avila/Chat/pull/7096)
- 👐 a11y: Improve Fork and SplitText Accessibility by **@danny-avila** in [#7147](https://github.com/danny-avila/Chat/pull/7147)
### 🔧 Fixes
- 🐛 fix: Avatar Type Definitions in Agent/Assistant Schemas by **@danny-avila** in [#6235](https://github.com/danny-avila/LibreChat/pull/6235)
- 🔧 fix: MeiliSearch Field Error and Patch Incorrect Import by #6210 by **@rubentalstra** in [#6245](https://github.com/danny-avila/LibreChat/pull/6245)
- 🔏 fix: Enhance Two-Factor Authentication by **@rubentalstra** in [#6247](https://github.com/danny-avila/LibreChat/pull/6247)
- 🐛 fix: Await saveMessage in abortMiddleware to ensure proper execution by **@sh4shii** in [#6248](https://github.com/danny-avila/LibreChat/pull/6248)
- 🔧 fix: Axios Proxy Usage And Bump `mongoose` by **@danny-avila** in [#6298](https://github.com/danny-avila/LibreChat/pull/6298)
- 🔧 fix: comment out MCP servers to resolve service run issues by **@KunalScriptz** in [#6316](https://github.com/danny-avila/LibreChat/pull/6316)
- 🔧 fix: Update Token Calculations and Mapping, MCP `env` Initialization by **@danny-avila** in [#6406](https://github.com/danny-avila/LibreChat/pull/6406)
- 🐞 fix: Agent "Resend" Message Attachments + Source Icon Styling by **@danny-avila** in [#6408](https://github.com/danny-avila/LibreChat/pull/6408)
- 🐛 fix: Prevent Crash on Duplicate Message ID by **@Odrec** in [#6392](https://github.com/danny-avila/LibreChat/pull/6392)
- 🔐 fix: Invalid Key Length in 2FA Encryption by **@rubentalstra** in [#6432](https://github.com/danny-avila/LibreChat/pull/6432)
- 🏗️ fix: Fix Agents Token Spend Race Conditions, Expand Test Coverage by **@danny-avila** in [#6480](https://github.com/danny-avila/LibreChat/pull/6480)
- 🔃 fix: Draft Clearing, Claude Titles, Remove Default Vision Max Tokens by **@danny-avila** in [#6501](https://github.com/danny-avila/LibreChat/pull/6501)
- 🔧 fix: Update username reference to use user.name in greeting display by **@rubentalstra** in [#6534](https://github.com/danny-avila/LibreChat/pull/6534)
- 🔧 fix: S3 Download Stream with Key Extraction and Blob Storage Encoding for Vision by **@danny-avila** in [#6557](https://github.com/danny-avila/LibreChat/pull/6557)
- 🔧 fix: Mistral type strictness for `usage` & update token values/windows by **@danny-avila** in [#6562](https://github.com/danny-avila/LibreChat/pull/6562)
- 🔧 fix: Consolidate Text Parsing and TTS Edge Initialization by **@danny-avila** in [#6582](https://github.com/danny-avila/LibreChat/pull/6582)
- 🔧 fix: Ensure continuation in image processing on base64 encoding from Blob Storage by **@danny-avila** in [#6619](https://github.com/danny-avila/LibreChat/pull/6619)
- ✉️ fix: Fallback For User Name In Email Templates by **@danny-avila** in [#6620](https://github.com/danny-avila/LibreChat/pull/6620)
- 🔧 fix: Azure Blob Integration and File Source References by **@rubentalstra** in [#6575](https://github.com/danny-avila/LibreChat/pull/6575)
- 🐛 fix: Safeguard against undefined addedEndpoints by **@wipash** in [#6654](https://github.com/danny-avila/LibreChat/pull/6654)
- 🤖 fix: Gemini 2.5 Vision Support by **@danny-avila** in [#6663](https://github.com/danny-avila/LibreChat/pull/6663)
- 🔄 fix: Avatar & Error Handling Enhancements by **@danny-avila** in [#6687](https://github.com/danny-avila/LibreChat/pull/6687)
- 🔧 fix: Chat Middleware, Zod Conversion, Auto-Save and S3 URL Refresh by **@danny-avila** in [#6720](https://github.com/danny-avila/LibreChat/pull/6720)
- 🔧 fix: Agent Capability Checks & DocumentDB Compatibility for Agent Resource Removal by **@danny-avila** in [#6726](https://github.com/danny-avila/LibreChat/pull/6726)
- 🔄 fix: Improve audio MIME type detection and handling by **@berry-13** in [#6707](https://github.com/danny-avila/LibreChat/pull/6707)
- 🪺 fix: Update Role Handling due to New Schema Shape by **@danny-avila** in [#6774](https://github.com/danny-avila/LibreChat/pull/6774)
- 🗨️ fix: Show ModelSpec Greeting by **@berry-13** in [#6770](https://github.com/danny-avila/LibreChat/pull/6770)
- 🔧 fix: Keyv and Proxy Issues, and More Memory Optimizations by **@danny-avila** in [#6867](https://github.com/danny-avila/LibreChat/pull/6867)
- ✨ fix: Implement dynamic text sizing for greeting and name display by **@berry-13** in [#6833](https://github.com/danny-avila/LibreChat/pull/6833)
- 📝 fix: Mistral OCR Image Support and Azure Agent Titles by **@danny-avila** in [#6901](https://github.com/danny-avila/LibreChat/pull/6901)
- 📢 fix: Invalid `engineTTS` and Conversation State on Navigation by **@berry-13** in [#6904](https://github.com/danny-avila/LibreChat/pull/6904)
- 🛠️ fix: Improve Accessibility and Display of Conversation Menu by **@danny-avila** in [#6913](https://github.com/danny-avila/LibreChat/pull/6913)
- 🔧 fix: Agent Resource Form, Convo Menu Style, Ensure Draft Clears on Submission by **@danny-avila** in [#6925](https://github.com/danny-avila/LibreChat/pull/6925)
- 🔀 fix: MCP Improvements, Auto-Save Drafts, Artifact Markup by **@danny-avila** in [#7040](https://github.com/danny-avila/LibreChat/pull/7040)
- 🐋 fix: Improve Deepseek Compatbility by **@danny-avila** in [#7132](https://github.com/danny-avila/LibreChat/pull/7132)
- 🐙 fix: Add Redis Ping Interval to Prevent Connection Drops by **@peeeteeer** in [#7127](https://github.com/danny-avila/LibreChat/pull/7127)
- 🐛 fix: Avatar Type Definitions in Agent/Assistant Schemas by **@danny-avila** in [#6235](https://github.com/danny-avila/Chat/pull/6235)
- 🔧 fix: MeiliSearch Field Error and Patch Incorrect Import by #6210 by **@rubentalstra** in [#6245](https://github.com/danny-avila/Chat/pull/6245)
- 🔏 fix: Enhance Two-Factor Authentication by **@rubentalstra** in [#6247](https://github.com/danny-avila/Chat/pull/6247)
- 🐛 fix: Await saveMessage in abortMiddleware to ensure proper execution by **@sh4shii** in [#6248](https://github.com/danny-avila/Chat/pull/6248)
- 🔧 fix: Axios Proxy Usage And Bump `mongoose` by **@danny-avila** in [#6298](https://github.com/danny-avila/Chat/pull/6298)
- 🔧 fix: comment out MCP servers to resolve service run issues by **@KunalScriptz** in [#6316](https://github.com/danny-avila/Chat/pull/6316)
- 🔧 fix: Update Token Calculations and Mapping, MCP `env` Initialization by **@danny-avila** in [#6406](https://github.com/danny-avila/Chat/pull/6406)
- 🐞 fix: Agent "Resend" Message Attachments + Source Icon Styling by **@danny-avila** in [#6408](https://github.com/danny-avila/Chat/pull/6408)
- 🐛 fix: Prevent Crash on Duplicate Message ID by **@Odrec** in [#6392](https://github.com/danny-avila/Chat/pull/6392)
- 🔐 fix: Invalid Key Length in 2FA Encryption by **@rubentalstra** in [#6432](https://github.com/danny-avila/Chat/pull/6432)
- 🏗️ fix: Fix Agents Token Spend Race Conditions, Expand Test Coverage by **@danny-avila** in [#6480](https://github.com/danny-avila/Chat/pull/6480)
- 🔃 fix: Draft Clearing, Claude Titles, Remove Default Vision Max Tokens by **@danny-avila** in [#6501](https://github.com/danny-avila/Chat/pull/6501)
- 🔧 fix: Update username reference to use user.name in greeting display by **@rubentalstra** in [#6534](https://github.com/danny-avila/Chat/pull/6534)
- 🔧 fix: S3 Download Stream with Key Extraction and Blob Storage Encoding for Vision by **@danny-avila** in [#6557](https://github.com/danny-avila/Chat/pull/6557)
- 🔧 fix: Mistral type strictness for `usage` & update token values/windows by **@danny-avila** in [#6562](https://github.com/danny-avila/Chat/pull/6562)
- 🔧 fix: Consolidate Text Parsing and TTS Edge Initialization by **@danny-avila** in [#6582](https://github.com/danny-avila/Chat/pull/6582)
- 🔧 fix: Ensure continuation in image processing on base64 encoding from Blob Storage by **@danny-avila** in [#6619](https://github.com/danny-avila/Chat/pull/6619)
- ✉️ fix: Fallback For User Name In Email Templates by **@danny-avila** in [#6620](https://github.com/danny-avila/Chat/pull/6620)
- 🔧 fix: Azure Blob Integration and File Source References by **@rubentalstra** in [#6575](https://github.com/danny-avila/Chat/pull/6575)
- 🐛 fix: Safeguard against undefined addedEndpoints by **@wipash** in [#6654](https://github.com/danny-avila/Chat/pull/6654)
- 🤖 fix: Gemini 2.5 Vision Support by **@danny-avila** in [#6663](https://github.com/danny-avila/Chat/pull/6663)
- 🔄 fix: Avatar & Error Handling Enhancements by **@danny-avila** in [#6687](https://github.com/danny-avila/Chat/pull/6687)
- 🔧 fix: Chat Middleware, Zod Conversion, Auto-Save and S3 URL Refresh by **@danny-avila** in [#6720](https://github.com/danny-avila/Chat/pull/6720)
- 🔧 fix: Agent Capability Checks & DocumentDB Compatibility for Agent Resource Removal by **@danny-avila** in [#6726](https://github.com/danny-avila/Chat/pull/6726)
- 🔄 fix: Improve audio MIME type detection and handling by **@berry-13** in [#6707](https://github.com/danny-avila/Chat/pull/6707)
- 🪺 fix: Update Role Handling due to New Schema Shape by **@danny-avila** in [#6774](https://github.com/danny-avila/Chat/pull/6774)
- 🗨️ fix: Show ModelSpec Greeting by **@berry-13** in [#6770](https://github.com/danny-avila/Chat/pull/6770)
- 🔧 fix: Keyv and Proxy Issues, and More Memory Optimizations by **@danny-avila** in [#6867](https://github.com/danny-avila/Chat/pull/6867)
- ✨ fix: Implement dynamic text sizing for greeting and name display by **@berry-13** in [#6833](https://github.com/danny-avila/Chat/pull/6833)
- 📝 fix: Mistral OCR Image Support and Azure Agent Titles by **@danny-avila** in [#6901](https://github.com/danny-avila/Chat/pull/6901)
- 📢 fix: Invalid `engineTTS` and Conversation State on Navigation by **@berry-13** in [#6904](https://github.com/danny-avila/Chat/pull/6904)
- 🛠️ fix: Improve Accessibility and Display of Conversation Menu by **@danny-avila** in [#6913](https://github.com/danny-avila/Chat/pull/6913)
- 🔧 fix: Agent Resource Form, Convo Menu Style, Ensure Draft Clears on Submission by **@danny-avila** in [#6925](https://github.com/danny-avila/Chat/pull/6925)
- 🔀 fix: MCP Improvements, Auto-Save Drafts, Artifact Markup by **@danny-avila** in [#7040](https://github.com/danny-avila/Chat/pull/7040)
- 🐋 fix: Improve Deepseek Compatbility by **@danny-avila** in [#7132](https://github.com/danny-avila/Chat/pull/7132)
- 🐙 fix: Add Redis Ping Interval to Prevent Connection Drops by **@peeeteeer** in [#7127](https://github.com/danny-avila/Chat/pull/7127)
### ⚙️ Other Changes
- 📦 refactor: Move DB Models to `@librechat/data-schemas` by **@rubentalstra** in [#6210](https://github.com/danny-avila/LibreChat/pull/6210)
- 📦 chore: Patch `axios` to address CVE-2025-27152 by **@danny-avila** in [#6222](https://github.com/danny-avila/LibreChat/pull/6222)
- ⚠️ refactor: Use Error Content Part Instead Of Throwing Error for Agents by **@danny-avila** in [#6262](https://github.com/danny-avila/LibreChat/pull/6262)
- 🏃♂️ refactor: Improve Agent Run Context & Misc. Changes by **@danny-avila** in [#6448](https://github.com/danny-avila/LibreChat/pull/6448)
- 📝 docs: librechat.example.yaml by **@ineiti** in [#6442](https://github.com/danny-avila/LibreChat/pull/6442)
- 🏃♂️ refactor: More Agent Context Improvements during Run by **@danny-avila** in [#6477](https://github.com/danny-avila/LibreChat/pull/6477)
- 🔃 refactor: Allow streaming for `o1` models by **@danny-avila** in [#6509](https://github.com/danny-avila/LibreChat/pull/6509)
- 🔧 chore: `Vite` Plugin Upgrades & Config Optimizations by **@rubentalstra** in [#6547](https://github.com/danny-avila/LibreChat/pull/6547)
- 🔧 refactor: Consolidate Logging, Model Selection & Actions Optimizations, Minor Fixes by **@danny-avila** in [#6553](https://github.com/danny-avila/LibreChat/pull/6553)
- 🎨 style: Address Minor UI Refresh Issues by **@berry-13** in [#6552](https://github.com/danny-avila/LibreChat/pull/6552)
- 🔧 refactor: Enhance Model & Endpoint Configurations with Global Indicators 🌍 by **@berry-13** in [#6578](https://github.com/danny-avila/LibreChat/pull/6578)
- 💬 style: Chat UI, Greeting, and Message adjustments by **@berry-13** in [#6612](https://github.com/danny-avila/LibreChat/pull/6612)
- ⚡ refactor: DocumentDB Compatibility for Balance Updates by **@danny-avila** in [#6673](https://github.com/danny-avila/LibreChat/pull/6673)
- 🧹 chore: Update ESLint rules for React hooks by **@rubentalstra** in [#6685](https://github.com/danny-avila/LibreChat/pull/6685)
- 🪙 chore: Update Gemini Pricing by **@RedwindA** in [#6731](https://github.com/danny-avila/LibreChat/pull/6731)
- 🪺 refactor: Nest Permission fields for Roles by **@rubentalstra** in [#6487](https://github.com/danny-avila/LibreChat/pull/6487)
- 📦 chore: Update `caniuse-lite` dependency to version 1.0.30001706 by **@rubentalstra** in [#6482](https://github.com/danny-avila/LibreChat/pull/6482)
- ⚙️ refactor: OAuth Flow Signal, Type Safety, Tool Progress & Updated Packages by **@danny-avila** in [#6752](https://github.com/danny-avila/LibreChat/pull/6752)
- 📦 chore: bump vite from 6.2.3 to 6.2.5 by **@dependabot[bot]** in [#6745](https://github.com/danny-avila/LibreChat/pull/6745)
- 💾 chore: Enhance Local Storage Handling and Update MCP SDK by **@danny-avila** in [#6809](https://github.com/danny-avila/LibreChat/pull/6809)
- 🤖 refactor: Improve Agents Memory Usage, Bump Keyv, Grok 3 by **@danny-avila** in [#6850](https://github.com/danny-avila/LibreChat/pull/6850)
- 💾 refactor: Enhance Memory In Image Encodings & Client Disposal by **@danny-avila** in [#6852](https://github.com/danny-avila/LibreChat/pull/6852)
- 🔁 refactor: Token Event Handler and Standardize `maxTokens` Key by **@danny-avila** in [#6886](https://github.com/danny-avila/LibreChat/pull/6886)
- 🔍 refactor: Search & Message Retrieval by **@berry-13** in [#6903](https://github.com/danny-avila/LibreChat/pull/6903)
- 🎨 style: standardize dropdown styling & fix z-Index layering by **@berry-13** in [#6939](https://github.com/danny-avila/LibreChat/pull/6939)
- 📙 docs: CONTRIBUTING.md by **@dblock** in [#6831](https://github.com/danny-avila/LibreChat/pull/6831)
- 🧭 refactor: Modernize Nav/Header by **@danny-avila** in [#7094](https://github.com/danny-avila/LibreChat/pull/7094)
- 🪶 refactor: Chat Input Focus for Conversation Navigations & ChatForm Optimizations by **@danny-avila** in [#7100](https://github.com/danny-avila/LibreChat/pull/7100)
- 🔃 refactor: Streamline Navigation, Message Loading UX by **@danny-avila** in [#7118](https://github.com/danny-avila/LibreChat/pull/7118)
- 📜 docs: Unreleased changelog by **@github-actions[bot]** in [#6265](https://github.com/danny-avila/LibreChat/pull/6265)
- 📦 refactor: Move DB Models to `@hanzochat/data-schemas` by **@rubentalstra** in [#6210](https://github.com/danny-avila/Chat/pull/6210)
- 📦 chore: Patch `axios` to address CVE-2025-27152 by **@danny-avila** in [#6222](https://github.com/danny-avila/Chat/pull/6222)
- ⚠️ refactor: Use Error Content Part Instead Of Throwing Error for Agents by **@danny-avila** in [#6262](https://github.com/danny-avila/Chat/pull/6262)
- 🏃♂️ refactor: Improve Agent Run Context & Misc. Changes by **@danny-avila** in [#6448](https://github.com/danny-avila/Chat/pull/6448)
- 📝 docs: chat.example.yaml by **@ineiti** in [#6442](https://github.com/danny-avila/Chat/pull/6442)
- 🏃♂️ refactor: More Agent Context Improvements during Run by **@danny-avila** in [#6477](https://github.com/danny-avila/Chat/pull/6477)
- 🔃 refactor: Allow streaming for `o1` models by **@danny-avila** in [#6509](https://github.com/danny-avila/Chat/pull/6509)
- 🔧 chore: `Vite` Plugin Upgrades & Config Optimizations by **@rubentalstra** in [#6547](https://github.com/danny-avila/Chat/pull/6547)
- 🔧 refactor: Consolidate Logging, Model Selection & Actions Optimizations, Minor Fixes by **@danny-avila** in [#6553](https://github.com/danny-avila/Chat/pull/6553)
- 🎨 style: Address Minor UI Refresh Issues by **@berry-13** in [#6552](https://github.com/danny-avila/Chat/pull/6552)
- 🔧 refactor: Enhance Model & Endpoint Configurations with Global Indicators 🌍 by **@berry-13** in [#6578](https://github.com/danny-avila/Chat/pull/6578)
- 💬 style: Chat UI, Greeting, and Message adjustments by **@berry-13** in [#6612](https://github.com/danny-avila/Chat/pull/6612)
- ⚡ refactor: DocumentDB Compatibility for Balance Updates by **@danny-avila** in [#6673](https://github.com/danny-avila/Chat/pull/6673)
- 🧹 chore: Update ESLint rules for React hooks by **@rubentalstra** in [#6685](https://github.com/danny-avila/Chat/pull/6685)
- 🪙 chore: Update Gemini Pricing by **@RedwindA** in [#6731](https://github.com/danny-avila/Chat/pull/6731)
- 🪺 refactor: Nest Permission fields for Roles by **@rubentalstra** in [#6487](https://github.com/danny-avila/Chat/pull/6487)
- 📦 chore: Update `caniuse-lite` dependency to version 1.0.30001706 by **@rubentalstra** in [#6482](https://github.com/danny-avila/Chat/pull/6482)
- ⚙️ refactor: OAuth Flow Signal, Type Safety, Tool Progress & Updated Packages by **@danny-avila** in [#6752](https://github.com/danny-avila/Chat/pull/6752)
- 📦 chore: bump vite from 6.2.3 to 6.2.5 by **@dependabot[bot]** in [#6745](https://github.com/danny-avila/Chat/pull/6745)
- 💾 chore: Enhance Local Storage Handling and Update MCP SDK by **@danny-avila** in [#6809](https://github.com/danny-avila/Chat/pull/6809)
- 🤖 refactor: Improve Agents Memory Usage, Bump Keyv, Grok 3 by **@danny-avila** in [#6850](https://github.com/danny-avila/Chat/pull/6850)
- 💾 refactor: Enhance Memory In Image Encodings & Client Disposal by **@danny-avila** in [#6852](https://github.com/danny-avila/Chat/pull/6852)
- 🔁 refactor: Token Event Handler and Standardize `maxTokens` Key by **@danny-avila** in [#6886](https://github.com/danny-avila/Chat/pull/6886)
- 🔍 refactor: Search & Message Retrieval by **@berry-13** in [#6903](https://github.com/danny-avila/Chat/pull/6903)
- 🎨 style: standardize dropdown styling & fix z-Index layering by **@berry-13** in [#6939](https://github.com/danny-avila/Chat/pull/6939)
- 📙 docs: CONTRIBUTING.md by **@dblock** in [#6831](https://github.com/danny-avila/Chat/pull/6831)
- 🧭 refactor: Modernize Nav/Header by **@danny-avila** in [#7094](https://github.com/danny-avila/Chat/pull/7094)
- 🪶 refactor: Chat Input Focus for Conversation Navigations & ChatForm Optimizations by **@danny-avila** in [#7100](https://github.com/danny-avila/Chat/pull/7100)
- 🔃 refactor: Streamline Navigation, Message Loading UX by **@danny-avila** in [#7118](https://github.com/danny-avila/Chat/pull/7118)
- 📜 docs: Unreleased changelog by **@github-actions[bot]** in [#6265](https://github.com/danny-avila/Chat/pull/6265)
AI-powered chat platform with enterprise features, using Hanzo's cloud API or local deployment.
# Hanzo Chat
## Quick Start
The chat surface of the Hanzo AI cloud: multi-model chat with agents, tools, and retrieval, running on Hanzo's backend. Live at [hanzo.chat](https://hanzo.chat).
Hanzo Chat is a sibling to [hanzo.app](https://hanzo.app) (the app builder) and the Hanzo console (admin). All inference, code execution, and web search route through the unified Hanzo API at `api.hanzo.ai/v1`, and sign-in is federated to Hanzo IAM ([hanzo.id](https://hanzo.id)).
## Features
- **Multi-model chat** — the Zen model family and other frontier models, served through `api.hanzo.ai`.
- **Agents** — build agents in the thread, or run your Hanzo Cloud agents (`/v1/agents`) with an `/agent` command or `@mention`.
- **MCP tools** — connect Model Context Protocol servers for tool use.
- **RAG** — chat over your own files and documents.
- **Web search** — grounded answers via Hanzo web search.
- **Code interpreter** — run code in a sandboxed runtime.
- **Image generation** — generate images inline.
- **Guest chat** — try a free Zen model with no account (optional, off by default).
## Requirements
- Node.js 24 (see `.nvmrc`)
- pnpm 10
- A Hanzo API key — get one at [hanzo.ai/dashboard](https://hanzo.ai/dashboard)
## Quick start (Docker)
```bash
# Clone and setup
git clone https://github.com/hanzoai/chat.git
cd chat
# Copy environment template
cp .env.example .env
# Edit .env and add your Hanzo API key
# Get your key at: https://hanzo.ai/dashboard
nano .env
# Start the platform
cp .env.example .env # set HANZO_API_KEY
make up
```
Access the chat at http://localhost:3081
Open http://localhost:3080. `make up` starts the full stack (app, MongoDB, Meilisearch) from `compose.yml`; `make down` stops it.
## Development
### Basic Development (with hot reload)
```bash
make dev
pnpm install # install workspace dependencies
pnpm build:packages # build the shared workspace packages
pnpm backend:dev # API server on :3080 (nodemon)
pnpm frontend:dev # Vite client dev server (second terminal)
@@ -49,7 +49,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- Mermaid Diagrams: "application/vnd.mermaid"
- The user interface will render Mermaid diagrams placed within the artifact tags.
@@ -63,7 +63,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
6. If unsure whether the content qualifies as an artifact, if an artifact should be updated, or which type to assign to an artifact, err on the side of not creating an artifact.
@@ -162,7 +162,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- SVG: "image/svg+xml"
- The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags.
@@ -186,7 +186,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- When iterating on code, ensure that the code is complete and functional without any snippets, placeholders, or ellipses.
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
@@ -367,7 +367,7 @@ Artifacts are for substantial, self-contained content that users might modify or
4. Add a \`type\` attribute to specify the type of content the artifact represents. Assign one of the following values to the \`type\` attribute:
- HTML: "text/html"
- The user interface can render single file HTML pages placed within the artifact tags. HTML, JS, and CSS should be in a single file when using the \`text/html\` type.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- The only place external scripts can be imported from is https://cdnjs.cloudflare.com
- SVG: "image/svg+xml"
- The user interface will render the Scalable Vector Graphics (SVG) image within the artifact tags.
@@ -391,7 +391,7 @@ Artifacts are for substantial, self-contained content that users might modify or
- The assistant can use prebuilt components from the \`shadcn/ui\` library after it is imported: \`import { Alert, AlertDescription, AlertTitle, AlertDialog, AlertDialogAction } from '/components/ui/alert';\`. If using components from the shadcn/ui library, the assistant mentions this to the user and offers to help them install the components if necessary.
- Components MUST be imported from \`/components/ui/name\` and NOT from \`/components/name\` or \`@/components/ui/name\`.
- NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/api/placeholder/400/320" alt="placeholder" />\`
- Images from the web are not allowed, but you can use placeholder images by specifying the width and height like so \`<img src="/v1/chat/placeholder/400/320" alt="placeholder" />\`
- When iterating on code, ensure that the code is complete and functional without any snippets, placeholders, or ellipses.
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.