mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-06 06:28:10 +00:00
1621 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9e6d677751
|
⚙️ ci: Bump GitHub Actions to Node.js 24 Runtimes (#14689)
* ci: bump GitHub Actions to Node.js 24 runtimes Clear Node 20 deprecation warnings on runners by moving workflow actions to majors that declare node24 (checkout, cache, setup-node, artifacts, Docker buildx/build-push/qemu/login, github-script, setup-go, Azure login/helm, create-pull-request, axe-linter). * fix: release leader lock via ioredis on Redis Cluster @keyv/redis EVAL can surface unhandled MOVED redirects on cluster, so resign() logged failure and left LeadingServerUUID set. Use ioredis for leader election SET NX / GET / Lua (same pattern as principals and concurrency locks) so cluster redirects are retried and resignation clears the lock. |
||
|
|
c570517768
|
🐘 test: refresh FerretDB harness, registry-derived models, bulkWrite coverage (#14679)
* fix(data-schemas): refresh FerretDB harness model coverage, fix compile errors, add bulkWrite differentials Track 2 of the search-stack plan (PLAN.md "FerretDB track"): - Replace the three hand-rolled 29-model MODEL_SCHEMAS maps in multiTenancy/sharding/orgOperations.ferretdb.spec.ts with a shared getModelSchemas(mongoose) helper (misc/ferretdb/schemas.ts) derived from the live createModels() registry, so coverage tracks all 37 current models automatically instead of drifting. Matches the reference pattern in misc/documentdb/compat.documentdb.spec.ts. - Fix the 3 compile-broken specs this uncovered: all three imported a `projectSchema` from '~/schema' that no longer exists (superseded by `chatProjectSchema`), which `tsc --noEmit` flags as TS2724 but the babel-based jest transform silently let through as `undefined`. Removing the hand-rolled maps removes the bad import as a side effect; verified clean with tsc across misc/ferretdb and misc/documentdb. - Add misc/ferretdb/bulkWrite.ferretdb.spec.ts: differential specs for the five bulkWrite flows the plan names as actually at risk (import via bulkSaveConvos/bulkSaveMessages, bulkWriteAclEntries, bulkIncrementTagCounts, Transaction.insertMany, file-TTL bulkWrite via extendFilesTTL). Each flow runs identical operations against a real mongodb-memory-server (always) and, when FERRETDB_URI is set, against FerretDB, asserting normalized result equality. Multi-document transactions already degrade via the existing supportsTransactions probe — not duplicated here. - Land the Spike A BSON-legibility findings (bson-legibility.md, bson-inventory.txt) from the bson-legibility-spike-6e38de worktree so decision 2's evidence is in-repo. Verified against a real FerretDB 2.7.0 + postgres-documentdb 17 stack (docker compose -f misc/ferretdb/docker-compose.ferretdb.yml): all 10 harness spec files pass individually, including all 10 bulkWrite.ferretdb tests (5 mongodb-memory-server baselines + 5 FerretDB differentials). Full packages/data-schemas src/ suite (1907 tests) unaffected. * 📝 docs: make the BSON projection findings self-contained The doc was written for readers who already knew the internal shorthand — it opened on "Spike A executed, Spike B scoped" and referred to Options 1/2/3 and "the handoff" without ever defining them, so a reader arriving from the repo could not follow the argument or act on the recommendation. Reframed around what the document actually investigates: the question is stated up front, the three candidate mechanisms are named in a table before they are compared, and the recommendation refers to them by name. No findings, numbers, or SQL changed. * 📝 docs: drop internal planning references from spec header * fix(data-schemas): keep FerretDB harness schema derivation side-effect free `getModelSchemas()` derived its map by calling `createModels(mongoose)`, which carried three consequences the harness did not want: - Model creation applies the tenant-isolation plugin to the module-level schema singletons, so every harness read and write inherited middleware that throws under `TENANT_ISOLATION_STRICT=true`. - Registering on the default connection meant the benchmark's own `mongoose.connect()` auto-created 37 collections in the URI's base database, adding a database and dozens of collections to the very catalog metrics it measures. - The unfiltered registry provisioned app-wide control-plane models (`SystemGrant`, `AuditLog`, `SkillSyncCredential`, `SkillSyncStatus`) into every org database. The helper now builds the registry on a throwaway Mongoose instance, returns schemas rebuilt from their own definition, options, and declared indexes, skips the four app-wide models (validated against the registry so a rename fails loudly), and memoizes the result. Also in this pass: - The "adds a new collection" migration test used `AuditLog`, which provisioning had already created, so it silently reused the production model and ignored its proposed schema. It now uses a fixture model absent from the registry and asserts the collection is missing beforehand and carries the proposed compound index afterwards. - `bulkWrite` flows run inside `runAsSystem()`; they drive production methods unscoped, as a cross-tenant maintenance job does, and otherwise fail closed under strict tenant isolation. - Phase 2's sparse-index assertion pinned a count the User schema no longer declares; it now checks that each index type round-trips. |
||
|
|
1596df724a
|
chore: Remove Published Credential Defaults (#14680) | ||
|
|
0db511fee8
|
♿ fix: Restore WCAG AA Contrast for Text Tokens & Hide Edit Action While Streaming (#14677)
* ♿ fix: Restore WCAG AA Contrast for Text Tokens & Hide Edit Action While Streaming Fixes the unreadable composer placeholder and the edit pencil that appears on hover mid-generation, plus the sibling token failures found while tracing the root cause. Placeholder: #13879 moved the composer from `dark:placeholder-white/60` to the semantic `placeholder:text-text-tertiary`, but `--text-tertiary` was `var(--gray-500)` in *both* themes, and #595959 is a dark gray. Dark mode fell from 5.90:1 to 1.91:1. Fixed at the token (dark -> gray-400, 4.56:1) rather than the call site: the token has 99 usages and was failing at 1.91-2.77:1 on every dark surface. The .gizmo dark theme already uses a light gray (#999999) for the same token, so only the default dark theme carried the inverted value. Two more instances of the same "token never tuned per theme" bug: - `--text-warning` was amber-500 in both themes: 2.15:1 in light across 13 real warning strings. Now amber-700 (5.02:1). - Light `status-{success,warning,error}` on their own `-subtle` fill measured 3.58 / 3.07 / 4.41 -- the exact pairing Alert, Badge, Tag and Chip use for every status variant. Bumped to the 700 ramp (5.21 / 4.84 / 5.91). Solid `bg-status-*` is only used for dots, so nothing renders text on it. Edit action: `hideEditButton` already covers `isSubmitting` and the button got `isVisible={false}` -> `opacity-0`, but `group-hover:opacity-100` (0,2,0) outranks bare `opacity-0` (0,1,0), so hovering the row revealed a disabled pencil. The reveal classes are now gated on `isVisible`, with `pointer-events-none` so the hidden button is inert. Both token sources of truth (style.css and themes/*.ts) were updated and verified in sync across all 67 tokens. Tests: new HoverButtons spec covers both hover states; semanticTokens.spec.ts gains a contrast guardrail over text tokens x surfaces and each status hue against its subtle fill, verified to fail on the original values. applyTheme.spec.ts now derives its expectation from the theme object instead of pinning a hex, so retuning a hue no longer breaks an unrelated plumbing test. * 🔤 style: Sort imports in HoverButtons spec CI's changed-files import-order check flagged the new spec; the previous commit bypassed the lint-staged hook that would have caught it. |
||
|
|
5ff46d8c67
|
🛟 fix: Stop Agents When Code Resources Cannot Recover (#14651)
* fix: Block Agents When Code Resources Cannot Recover * fix: Preserve Resource Recovery Failures Across Agent Paths * fix: Centralize Fatal Agent Initialization * chore: sort agent imports |
||
|
|
6d2f29266c
|
🔑 feat: Refresh-Capable Google Admin OAuth Sessions (#13832)
* 🔑 feat: Refresh-Capable Google Admin OAuth Sessions Google admin sessions cannot be refreshed today. Three gaps add up to that: passport.authenticate('googleAdmin', ...) in api/server/routes/admin/auth.js never sets access_type=offline, so Google omits the refresh_token from its token response; createOAuthHandler in api/server/controllers/auth/oauth.js only forwards a refresh token into the admin exchange payload when the user's provider is 'openid' AND OPENID_REUSE_TOKENS is enabled; and /api/admin/oauth/refresh is openid-only, calling openid-client.refreshTokenGrant against the configured OIDC issuer. OpenID admins refresh transparently because all three are in place for them. This PR closes all three. The googleAdmin authenticate call now passes accessType: 'offline' and prompt: 'consent' so Google issues a refresh token on consent; the chat-side googleLogin is untouched. The shared socialLogin verify callback now passes the IdP refreshToken through as passport's third argument (info), landing on req.authInfo, with the two-argument call shape preserved when no refresh token is present so existing strategy tests stay valid. createOAuthHandler reads req.authInfo?.refreshToken for non-OpenID admin providers and forwards it into the exchange code; the OpenID branch and its OPENID_REUSE_TOKENS gate are unchanged. /api/admin/oauth/refresh now accepts an optional provider field ('openid' | 'google', default 'openid'). The new Google branch POSTs grant_type=refresh_token to https://oauth2.googleapis.com/token, decodes the returned id_token for the sub claim, looks up the admin user by googleId, enforces tenant scope and ACCESS_ADMIN, and mints a fresh LibreChat JWT in the same response shape /oauth/exchange returns. It is gated on GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET being set (returns 503 GOOGLE_NOT_CONFIGURED otherwise); unknown provider values return 400 INVALID_PROVIDER. * 🔁 fix: Harden Google admin refresh against bot review findings Five validated findings from the initial bot pass: socialLogin.js: mirror the OpenID migrate-or-reject pattern on the email fallback. When an existing user is found by email and the stored provider id is empty, persist the refreshed sub so the refresh path can later bind to it. When the stored id is present and differs, reject as AUTH_FAILED to prevent identity-swap, matching the existing OpenID behavior in packages/api/src/auth/openid.ts. oauth.js: scope the non-OpenID admin refresh-token forwarding to provider === 'google'. The previous else branch would have forwarded a Discord refresh token (passport-discord supplies one) into the admin exchange payload even though /api/admin/oauth/refresh only accepts openid or google, leaving the admin client with a token it could not refresh. admin/auth.js (refreshGoogleAdminSession): drop id_token from the mandatory-fields check. Google's OAuth refresh response is documented to include id_token only conditionally, so the previous mandatory check broke refresh whenever Google omitted it. Decode id_token when present (fast path); when absent, call Google's userinfo endpoint with the access token to read sub. Wrap tokenResponse.json() in try/catch and return IDP_INCOMPLETE on parse failure instead of a generic 500. Tighten access_token to a typeof string check. admin/auth.js (refreshGoogleAdminSession): reuse serializeUserForExchange for the response user so the Google refresh shape matches /oauth/exchange and the OpenID branch exactly (full _id, id, email, name, username, role, avatar, provider, openidId). The previous Google-specific subset dropped fields the admin client relies on for later provider-specific refreshes and disambiguation. Tests cover each fix: socialLogin's migration and rejection cases, the oauth.js Discord-gating case, the userinfo fallback path on missing id_token, CLAIMS_INCOMPLETE when both id_token and userinfo are absent, IDP_INCOMPLETE on a non-JSON token body, and the full response shape on the happy path. * 🧪 fix: Add updateUser to appleStrategy test mock for socialLogin migration The shared socialLogin verify callback now invokes `updateUser` when the email-fallback path discovers a same-provider user with an empty provider id, persisting the refreshed sub. The Apple strategy test's `~/models` mock did not stub `updateUser`, so the migration path hit `TypeError: updateUser is not a function` and failed the `should handle existing user and update avatarUrl` case in CI shard 1/3. * 🧹 refactor: Move Google admin refresh into TypeScript @librechat/api helper Per repo guidance (CLAUDE.md): all new backend code must be TypeScript in /packages/api, and /api is a thin JS wrapper. The previous commit landed the Google admin refresh flow as ~120 lines of new JS inside api/server/routes/admin/auth.js, which violates that. This commit extracts the flow into a new TS helper at packages/api/src/auth/googleRefresh.ts and reduces the route handler to a thin dep-wiring wrapper. The helper exports applyGoogleAdminRefresh(deps, options) with the same shape as the OpenID applyAdminRefresh: callers pass findUsers, getUserById, canAccessAdmin, and mintToken as deps so the package stays free of /api model imports and capability/session helpers. The route handler now builds those deps from the existing model + capability + token modules and calls the helper, mapping AdminRefreshError to the documented HTTP responses. While moving the code, the helper now guards getUserById with Types.ObjectId.isValid before the direct-lookup branch, matching the OpenID admin path at packages/api/src/auth/refresh.ts. Without this guard a malformed user_id from the admin client would hit Mongoose findById's CastError and surface as a 500 INTERNAL_ERROR instead of falling through to the documented sub-based lookup. Tests move with the code: packages/api/src/auth/googleRefresh.spec.ts now owns the helper's behavior (token endpoint, userinfo fallback, ObjectId guard, USER_ID_MISMATCH/TENANT_MISMATCH/USER_NOT_FOUND/FORBIDDEN, rotated refresh-token pass-through, GOOGLE_NOT_CONFIGURED, IDP_INCOMPLETE on non-JSON body, CLAIMS_INCOMPLETE when both id_token and userinfo miss). The route-level api/server/routes/admin/auth.refresh.test.js drops the duplicated end-to-end Google cases and keeps a smaller surface: route delegates to applyGoogleAdminRefresh with the right deps + options, maps AdminRefreshError to HTTP status/code, falls through to 500 for unknown errors, and rejects unknown providers with INVALID_PROVIDER. * 🔁 fix: Tighten Google admin refresh and limit social-login changes Brutal-review findings on top of the upstream feature work. socialLogin.js: the migrate-or-reject pattern from the previous commit applied to every provider's chat-side verify callback, not just the admin flow. Gate both branches on `options.existingUsersOnly` so the chat-side googleLogin / facebookLogin / etc. keep their pre-existing email-fallback behavior unchanged. Tests follow: restore the original `should fallback to finding user by email` chat-side case and re-add the migration and mismatch-reject cases as admin-only by passing `{ existingUsersOnly: true }` to socialLogin in those tests. googleRefresh.ts: add a defense-in-depth `isEmailAllowed(user)` dep that the helper invokes before `canAccessAdmin`. Mirrors the `isEmailDomainAllowed` check the initial Google admin login already runs, so a deployment that removes a domain from `registration.allowedDomains` after issuance can no longer mint fresh JWTs for that admin via refresh. The route handler wires it up with `resolveAppConfigForUser` + `isEmailDomainAllowed`, falling back to `baseOnly` config for users without a tenantId. googleRefresh.ts: drop the unreachable `?? ''` defensive coalescing in `fetchGoogleTokenset`. The `GOOGLE_NOT_CONFIGURED` guard upstream already narrows `clientId`/`clientSecret` to non-empty strings; the function takes a narrowed `GoogleAdminRefreshConfiguredOptions` shape and `applyGoogleAdminRefresh` constructs that shape after the guard. * 🔒 fix: Apply brutal-review hardening to Google admin refresh Tighten the Google OAuth refresh flow against all outstanding code review findings: enforce JWT aud claim verification against the configured clientId (ISSUER_MISMATCH on mismatch), reject ambiguous googleId matches (limit:2 in findUsers, USER_ID_MISMATCH when multiple rows match), scope the authInfo refresh-token carrier to the Google provider only, add TOCTOU re-read defense after the admin googleId migration write in socialLogin, deduplicate canAccessAdmin/mintToken closures via buildAdminRefreshClosures shared by both OpenID and Google refresh paths, document rotation semantics on AdminExchangeResponse.refreshToken, standardise all log prefixes to [admin/oauth/refresh], and expand test coverage for all new paths. * 🔒 fix: Reject refresh for users migrated off the Google provider The interactive Google admin login path in socialLogin.js already rejects a user whose provider field is not 'google', returning AUTH_FAILED. Without a matching guard in the refresh path, a user migrated to OpenID could use an unexpired Google refresh token to keep minting admin JWTs indefinitely. Add a PROVIDER_MISMATCH check after resolving the user in both the direct getUserById branch and the findUsers fallback branch of resolveAdminUser, mirroring the provider gate the interactive path enforces. * 🔒 fix: Add ban check and fix domain allowlist on admin OAuth refresh Two gaps in the /api/admin/oauth/refresh route: Add middleware.checkBan to the route chain before preAuthTenantMiddleware, matching the gate that /login/local and createOAuthHandler already apply. Without it a banned admin could keep minting JWTs until their IdP refresh token expired. Replace getAppConfig({ baseOnly: true }) in the non-tenant isEmailAllowed closure with getAppConfig({ role: user.role }), which includes DB-layer overrides from the admin panel. baseOnly returns only YAML-derived config, so any allowedDomains list maintained entirely through the admin panel was silently inert on this path. Extract isEmailAllowedForUser as a shared helper, move it into buildAdminRefreshClosures so both Google and OpenID refresh paths enforce domain policy consistently, and add isEmailAllowed to AdminRefreshDeps in the TS package so applyAdminRefresh can invoke it. * 🔒 fix: Harden admin OAuth refresh against user bans, tenant scope gaps, and cross-tenant migration Post-identity-resolution ban check: the initial checkBan middleware fires before the refresh token is exchanged and req.user is populated, so it can only evaluate IP bans. After applyGoogleAdminRefresh/applyAdminRefresh resolves the user identity, we now synthesize req.user and re-run checkBan against the resolved user's id before emitting the JWT, so a user-level ban is enforced even from a fresh IP. Domain allowlist now includes userId: the getAppConfig call in isEmailAllowedForUser was passing only role, missing user and group-level allowedDomains overrides that the initial OAuth callback's checkDomainAllowed enforces via userId. Both branches now pass userId so buildPrincipals takes the full user+group+role resolution path. The tenant branch is also inlined (replacing resolveAppConfigForUser) to accept userId, wrapped in tenantStorage.run for correct Mongoose scoping and cache-key resolution. Cross-tenant email-fallback migration: the Passport verify callback fires before tenantContextMiddleware, so findUser({email}) is unscoped and can return a same-email user from another tenant. Writing googleId onto that document permanently corrupts the other tenant's account. Migration is now blocked for users with a tenantId; single-tenant users are unaffected. --------- Co-authored-by: Danny Avila <danny@librechat.ai> |
||
|
|
bd5c1ad05c
|
🧩 refactor: Shared UI Design System Tokens (#14670)
* feat: harden shared design system tokens * fix: address design system review findings |
||
|
|
8d5c298bdf
|
🔗 fix: Prevent duplicate share rows from clobbering pending role edits (dedupe adds by stable id) (#14655)
Follow-up to #14316 / #14317. The share dialog deduped picker adds by raw idOnTheSource, but loaded ACL rows carry the external oid for OpenID/Entra users while search results carry the local _id (searchPrincipals dropped the selected idOnTheSource before transforming). Re-adding an already-listed user therefore appended a duplicate row, and the stable-id de-dup in computeShareChanges let the appended default-role row silently shadow a pending role edit (no-op PUT with a success toast). - Dedupe adds via a new dedupeNewShares helper keyed by principalKey, and skip marking the dialog dirty when nothing was added - Propagate idOnTheSource through the user-search transform so picker results match ACL rows (also fixes excludeIds filtering) - Add regression tests Closes #14654 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Danny Avila <danny@librechat.ai> |
||
|
|
c6bb77325a
|
🔗 feat: Admin Panel Link in Settings for Admins (#14662)
Expose ADMIN_PANEL_URL through the startup config for users holding the access:admin capability, and render an Admin section in Settings > General with an external link to the admin panel. The URL is omitted server-side for unauthenticated requests and users without admin access. |
||
|
|
33c8801ba6
|
🌊 feat: Wire Adaptive Stream Smoothing Across Google, Bedrock, and Zero-Disable Semantics (#14660)
* 🌊 feat: Wire Adaptive Stream Smoothing Across Google, Bedrock, and Zero-Disable Semantics With @librechat/agents 3.4.0 smoothing defaults ON (25ms adaptive) for every provider; this completes the LibreChat side: - google: fix the unguarded endpoints.all clobber and wire streamRate into llmConfig._lc_stream_delay — previously read from config and silently dropped, making google streamRate a no-op end to end - bedrock: wire streamRate (endpoint + endpoints.all) — previously absent entirely, so bedrock smoothing was unreachable from config - anthropic/openai: nullish guards so streamRate: 0 survives as the explicit smoothing disable; drop the azure 30/17 hardcoded fallback the SDK default now supersedes - delete dead createHandleLLMNewToken (no call sites since #6886, and LangChain backgrounds callbacks so a sleep there never paced anything) - schema: streamRate gains .min(0) and docs; example docs updated, including pairing STREAM_DELTA_COALESCE_MS with the smoothing tick * 🩹 fix: Address Review — Keep Published Shim, Typed Delay Access, Scoped Docs - restore createHandleLLMNewToken as a @deprecated compatibility shim: it ships in the public @librechat/api root, so removal is reserved for a major release - assign llmConfig._lc_stream_delay via the SDK's typed property (3.4.0 StreamSmoothingOptions) instead of Record<string, unknown> casts at all five sites - scope the 25ms-default wording to agents SDK-backed providers (legacy Assistants/Ollama still per-chunk sleep at DEFAULT_STREAM_RATE=1) and clarify coalescing guidance when streamRate: 0 |
||
|
|
4f5c9fec4f
|
🎨 refactor: adopt the @librechat/client design system (semantic color tokens + component migration) (#13879)
* refactor: unify Tailwind color tokens into a single source
Both the client SPA and @librechat/client Tailwind configs now consume one
createTailwindColors() map, eliminating config drift. Fixes the package-side
build along the way: shadcn tokens are wrapped in hsl(), the broken opacity
helper is removed, and text-destructive/border-destructive/switch-unchecked
plus the gray/green palettes are included.
* refactor: replace hardcoded colors in sidebar conversation list with tokens
Migrate the Conversations sidebar section to semantic tokens: focus rings to
ring-text-primary (keeps >=3:1 contrast in both modes; the mid-gray ring would
fail WCAG 1.4.11 on dark), the active-conversation indicator and hover-fade
gradient to surface/text tokens, and the pagination controls. Removes every
dark: color twin; no behavior change.
* feat: add semantic status-color tokens; migrate MCP status badge
Add a status-color layer (status-{success|info|warning|error|neutral} plus
-subtle variants) to style.css and the unified createTailwindColors map, with a
blue palette for the info hue. Migrate MCPStatusBadge (badges + dots) and
MCPCardActions to the new tokens, removing all hardcoded status colors and
dark: twins. Status colors are now themeable like the rest of the system.
* refactor: migrate status badges to semantic status-color tokens
Migrate the genuine status badges to the status-* tokens: MCPConfigDialog
connection pills (info/warning/neutral/error/success + dot), MemoryUsageBadge
usage levels, and DialogImage quality badge (also gains dark-mode support it
previously lacked). Removes hardcoded colors and dark: twins.
* feat: add Alert component and migrate alert banners to it
Add a reusable Alert component (@librechat/client) with error/success/warning/
info/neutral variants backed by the status-color tokens, default per-variant
icons, and role=alert. Migrate the duplicated colored-div banners to it:
Auth ErrorMessage, RequestPasswordReset success, and the identical error boxes
in ToolSelectDialog, AssistantToolsDialog, and MCPToolSelectDialog.
* refactor: migrate remaining alert banners and error states to tokens
Migrate the last banners to the Alert component: ResetPassword success,
MessageContent connection error, and MemoryInfo storage-full errors. Tokenize
the Agents ErrorDisplay error state in place (icon badge, headings, message,
retry button) since it's a full error state, not a compact callout. Also
tokenize ResetPassword field-validation errors to text-text-destructive
(fixes the low-contrast dark:text-red-900).
* refactor: tokenize SidePanel Memories/Parameters/Bookmarks colors
Delete-confirm buttons to surface-destructive tokens (MemoryCardActions,
BookmarkCardActions), drop redundant text-white on submit Buttons (the variant
already sets it), legacy preset button green hover/focus to submit tokens, and
slider hover borders to border-light. Leaves DynamicCheckbox dark overrides for
a separate pass against the Checkbox component.
* refactor: tokenize Settings danger/destructive buttons
Map the DangerButton, the Data tab destructive actions (RevokeKeys, ClearChats,
DeleteCache), and the DeleteAccount button from bg-red-*/bg-destructive to the
surface-destructive tokens.
* refactor: tokenize Chat file-upload table and upload status colors
Tokenize TemplateTable th/td/border classes (surface-primary, border-light,
text-primary/secondary) and FileUpload status colors (text-text-secondary,
text-text-destructive, text-status-success) plus the import button hover.
* fix: explicit type annotations on Alert for isolatedDeclarations
@librechat/client builds with tsdown --isolatedDeclarations, which requires
exported consts to have explicit type annotations (TS9010). Annotate
alertVariants and Alert to match the Button.tsx pattern.
* refactor: add soft status-border token layer for Alert and lighten dark status foregrounds
* refactor: tokenize Chat menus, popovers, and message surfaces
* refactor: tokenize Chat message content, tool output, and file UI colors
* refactor: add semantic link color token and migrate hyperlinks to it
* refactor: tokenize Files and Auth surfaces, text, borders, and CTAs
* refactor: add accent-primary brand token; tokenize Nav/Input/Prompts/Endpoints colors
* refactor: tokenize Auth brand-green accents, Skills, Sharing, Plugins, MCP colors
* refactor: tokenize OAuth, Share, ui, Bookmarks, Tools, Messages, Web, SharePoint colors
* refactor: final solid-color cleanup (brand-green accents, neutral grays, error text)
* refactor: migrate status callout banners to status-subtle/border tokens
* refactor: tokenize token-usage gauge, mic, and oauth countdown status colors
* refactor: replace shadcn color vocabulary with semantic tokens
Remove the shadcn/ui color tokens (background, foreground, card, popover,
muted, accent, secondary, destructive, input) and migrate every usage to
LibreChat semantic surface/text/border tokens.
Add surface-inverted/text-inverted for the neutral inverted CTA and
surface-fixed/text-fixed for controls that must not flip with the theme
(favicon chips, QR container, carousel arrows). New tokens are defined once
in style.css (light + dark), createTailwindColors, the theme types,
applyTheme and the default/dark theme objects so they stay overridable at
runtime.
Collapse paired dark: color variants into the dark-aware tokens and tokenize
the remaining raw palette and white/black utilities, mapping status colors to
the status-* tokens and legacy ring-black/ring-white focus rings to
ring-text-primary.
Retain the background, primary and ring tokens, which are still referenced by
the SidePanel/Agents and SidePanel/Builder panels (excluded from this pass).
* refactor: tokenize remaining status, neutral and message-text colors
Map the leftover semantic colors to tokens: skill error/dirty states and the
selected-version/selected-skill highlights move to status-warning/status-success,
the global indicator to status-success, and the markdown message text to
text-text-primary. Drop the redundant dark: overrides on the dynamic checkbox,
which the Checkbox primitive already handles.
What remains is intentional and stays raw: categorical color sets (category
icons, principal avatars, per-tool toggle accents), brand marks, the
WCAG-tuned toast severities, code/diagram surfaces, scrims, and text-white on
submit/destructive action surfaces.
* refactor: remove unused CSS rules, dead comments, and duplicate keyframes
Drop ~829 lines of dead styles across style.css (2992->2355) and
mobile.css (323->131): unreferenced classes (legacy token utilities,
orphaned animations, form/prose/scrollbar leftovers), commented-out
blocks, and duplicate/orphaned keyframes. Library-injected (hljs, sandpack,
codemirror, markdown language) and dynamically-applied (scroll-animation,
icon sizes) classes were retained.
* fix: resolve ESLint and frontend test failures
- Format with prettier (Alert, MCPStatusBadge, ApiKeys, Memory, etc.) after
--no-verify commits skipped the hook
- Localize the 'Or' auth divider (com_auth_or) instead of a bare literal
- Drop dead InvocationModePicker imports in Skill forms; fix VerifyEmail
unused arg + useEffect deps
- Revert out-of-scope color edits in legacy Files/VectorStore views that
carried pre-existing untranslated-string lint debt
- Update Memory tests to assert status-* tokens (text-status-error,
bg-status-error-subtle) instead of the old hardcoded red classes
* refactor: migrate theme tokens to RGB channels for opacity support
Convert semantic + palette CSS variable values in style.css from hex to bare
'R G B' channel triplets, and emit Tailwind colors as
rgb(var(--token) / <alpha-value>) via createTailwindColors. This makes opacity
modifiers (bg-surface-primary/50, bg-border-medium/60, etc.) resolve correctly
and remain dark-aware, fixing ~26 existing usages that previously fell back to a
hardcoded light hex.
- Wrap direct var(--token) color usages in CSS rules as rgb(var(--token))
(style.css, Dropdown.css, Tooltip.css) and two inline component styles
- applyTheme writes bare triplets to match the new wrapping
- shadcn tokens (HSL) and the JS palette (hex) are unchanged
* fix: prettier formatting after dev rebase
* refactor(client): migrate low-risk primitives to @librechat/client
Swap raw <label>, <textarea>, and native title= tooltips for the
@librechat/client Label, Textarea, and TooltipAnchor components across
Agents, Endpoints settings, Export modal, Prompts, Sharing, and Memory
dialogs. Add localization keys (scroll, sibling navigation, none
selected, select var) for the remaining swap waves.
* refactor(client): migrate buttons, inputs and labels to @librechat/client
Swap raw <button>, <input> and <label> elements for the @librechat/client
Button, Input and Label components across Auth, Chat, Conversations,
Endpoints, Nav, Prompts, Skills, Tools and Web. Preserve bespoke geometry
and behavior via cn className merging, keep data-testid/aria wiring, and
localize previously hardcoded aria-labels. Skip swaps that would break
floating-label animations, tiny bespoke controls or inline-text links.
Add com_ui_reload_page key.
* refactor(client): migrate dialogs, toggles and remaining controls to @librechat/client
Swap behavioral controls for @librechat/client equivalents: HeadlessUI
and legacy dialogs to OGDialog, native checkbox/switch to Checkbox/Switch
(onCheckedChange), and remaining buttons/inputs across Chat, Skills,
Tools, Sharing, Memories and Settings. Convert applicable native title=
tooltips to TooltipAnchor and localize close/scroll aria-labels. Skip
swaps that would break floating-label animations or bespoke select
behavior. Update co-located test mocks to provide the newly-used Button
and cn dependencies.
* style(client): soften dropdown and settings search inputs
Remove the heavy focus ring on the settings search and the searchable
Dropdown's search input, replacing it with a subtle border-light. Make
the search field background inherit the dropdown surface so it matches in
both light and dark mode, and reduce the Dropdown trigger border from
medium to light.
* refactor(client): migrate Agent Builder and Tool Library to @librechat/client
Swap raw buttons, inputs, labels, textareas and native title tooltips for
the @librechat/client Button/Input/Label/Textarea/TooltipAnchor components
across the Agent Builder panel (SidePanel/Agents) and the Tools
marketplace. Remove heavy input focus rings in favor of subtle borders,
soften dropdown trigger borders, and convert stray shadcn/raw colors in
touched lines to semantic tokens. Localize the tool delete aria-label and
toast messages. Update co-located test mocks to provide the newly-used
Button component.
* fix(client): keep Input border static on pointer focus
The pointer-focus override in Field.css used border-color: var(--border-light),
which became an invalid value after the theme moved to RGB channel tokens and
was silently dropped, letting the border fall back to currentColor (text-primary)
on mouse focus. Wrap it in rgb() so mouse focus produces no border, ring, or
outline change; keyboard focus keeps its ring for accessibility.
* refactor(client): remove residual shadcn color tokens
The background/primary/primary-foreground/ring and unused chart-* tokens were
retained only for the then-unmigrated Agent Builder. With that panel migrated,
replace the last usages with LibreChat semantic tokens (ring-primary/ring-ring
-> ring-text-primary; bg-primary/text-primary-foreground -> bg-surface-inverted
/text-text-inverted; text-primary -> text-text-primary; bg-background ->
bg-surface-primary) and drop the token definitions from createTailwindColors,
applyTheme, the theme objects, types, and style.css.
* fix(client): address semantic theme review feedback
* fix(client): use boolean Monaco hover option
* fix(client): resolve CI validation failures
* test(client): update shared component mocks
* fix(client): expose status tokens to runtime themes and document channel format
Add the status, text-destructive and border-destructive families to IThemeRGB,
IThemeVariables, IThemeColors, mapTheme and the bundled light/dark themes so
ThemeProvider consumers can theme Alert and the status badges instead of falling
back to the stylesheet palette.
Update the theme README to document the channel-triplet contract that the RGB
migration introduced, since the previous examples used complete CSS colors that
now produce invalid declarations.
* test(e2e): use accessible message action locators
* fix(client): address theme env, dialog padding and locked button review feedback
Expose every IThemeRGB token through REACT_APP_THEME_* instead of the
hand-maintained subset that omitted the status, destructive, inverted and
fixed families.
Drop the padding OGDialogContent contributes to the Tool Library so the
header divider spans the panel again, and stop disabled:opacity-100 from
overriding the locked delete-account button's dimmed state.
* fix(client): read theme environment variables from the build-time env
getThemeFromEnv read process.env, which vite-plugin-node-polyfills replaces
with an empty shim in the browser, so every REACT_APP_THEME_* value was
dropped and the loader always returned undefined.
Read import.meta.env instead and register the REACT_APP_THEME_ prefix with
Vite so the values are inlined at build time. The env source is now a
parameter, which lets the tests cover the mapping without mutating globals.
* fix(client): replace Tailwind classes that no longer resolve
Several class names in the client and shared component package emit no CSS
rule at all: legacy token- names with no definition, Tailwind v1/v4 names,
and plain typos. They fail silently past typecheck and tests.
- text-md -> text-base (Tailwind has no md font size)
- text-grey-100, text-tertiary -> text-text-tertiary
- text-token-secondary -> text-text-secondary
- bg-token-surface-primary/tertiary, bg-token-main-surface-secondary and
border-token-border-hover -> their semantic tokens
- bg-surface, bg-surface-50 -> bg-surface-primary
- bg-surface-primary-hover -> bg-surface-hover
- outline-hidden -> outline-none where focus styling already exists
- drop focus:shadow-outline, border-d-0 and the malformed
ring-offset-ring-offset, which have no meaningful replacement
MemoryArtifacts keeps its default outline instead of gaining outline-none,
since that button has no other focus indicator. MentionItem drops its dead
background rather than adopting one, which would have matched its hover
colour and erased the hover affordance.
Localize the two literal strings the pre-commit lint flagged in the touched
files, reusing the existing com_ui_upload_image and com_ui_more_count keys.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
|
||
|
|
96404bfc73
|
🛡️ fix: SSRF-Guard Speech (STT/TTS) and OCR Outbound Requests at Connect Time (#14560)
* fix: SSRF-guard speech (STT/TTS) and OCR outbound requests at connect time Speech (STT/TTS) and OCR issued outbound HTTP to operator-provided target URLs with only proxy config attached, so a target that resolves to a private, loopback, link-local, or cloud-metadata address was reachable by the server. This is the same class already guarded for the custom models fetch, Actions, avatar, MCP, and the OpenAI/Anthropic endpoint clients. Add one helper applySSRFSafeAgentIfDirect(config, url, allowedAddresses) that rejects non-http(s) or unparseable target URLs, sets maxRedirects to 0 so a redirect cannot bypass the connect-time check, and attaches createSSRFSafeAgents when no proxy or agent is already set (proxy precedence preserved). Wire it into the six speech/OCR call sites and thread each section's allowedAddresses exemption from pr-01. Scope is private/internal SSRF only. It does not restrict forwarding a credential to a public host, which is a separate egress-allowlist concern. Absent an allowedAddresses entry, private targets now fail closed, matching endpoints, actions, and MCP. Document the new fields in librechat.example.yaml with the operator warning that allowedAddresses hostnames are trusted before the private-IP check. * fix: block literal private-IP hosts and thread STT exemptions through generic uploads applySSRFSafeAgentIfDirect only attached the DNS-lookup agents, but Node skips the custom lookup for IP-literal hosts, so a literal private IP such as http://127.0.0.1 connected unchecked. Reject literal private IPs synchronously in the helper, reusing the same allowedAddresses exemption logic as the lookup path. The generic audio-upload path did not forward the section-level allowedAddresses to sttRequest, so a private STT endpoint permitted via speech.stt.allowedAddresses failed with ESSRF outside the speech route. Thread the exemption through files/audio.ts and widen the STTService type. * fix: derive effective SSRF port for literal IPs and validate OCR target before opening its stream The literal-IP precheck normalized an empty URL.port to '', so allowedAddresses exemptions on a default port (127.0.0.1:80, [::1]:443) never matched. Derive 80/443 from the scheme when the port is omitted. uploadDocumentToMistral opened the upload file stream before the SSRF check, so a blocked or malformed target threw with the descriptor still open. Run the proxy/SSRF setup before fs.createReadStream. Reword the librechat.example.yaml allowedAddresses guidance to prefer a private IP literal over a hostname, and reconcile the speech/OCR SSRF specs to assert the synchronous literal-IP block instead of driving the connect-time lookup that Node skips for IP literals. * fix: block literal private IPs before the proxy return, destroy OCR stream on failure, canonicalize IPv6 exemptions Move the literal-IP check above the proxy/agent early return so a literal private IP is rejected even when a proxy is configured; document that a forward proxy must be SSRF-enforcing. Wrap the Mistral upload post in try/finally and destroy the file stream, so an async connect-time block does not leak the descriptor. Canonicalize IP literals in normalizeAddressCandidate through the same URL serialization targets use, so IPv4-mapped and expanded IPv6 exemptions match. |
||
|
|
0e14d91ed9
|
⏱️ fix: Compile admin file-config MIME patterns on a linear-time engine (ReDoS) (#14555)
* ⏱️ fix: Compile admin file-config MIME patterns on a linear-time engine convertStringsToRegex compiled admin-configured supportedMimeTypes with the native RegExp engine, and checkType runs those patterns against an uploaded file's Content-Type on the server event loop, so a catastrophic-backtracking pattern in fileConfig could ReDoS the whole process on upload. The MIME-pattern compiler is now swappable. It defaults to native RegExp, which browser builds keep so no engine is added to the client bundle, and the server injects a linear-time engine (RE2JS) at startup. Only test is ever called on these matchers, so the shared type widens to a structural RegexLike with no behavior change for valid patterns. The browser stays on native because a client-side stall would only affect that one tab. * ⏱️ fix: Wire the linear MIME compiler in the experimental entry point api/server/experimental.js mounts the same upload routes and calls mergeFileConfig but never set the linear-time compiler, so admin MIME patterns still compiled with native RegExp there. Mirror the setup, and widen the client-side supportedMimeTypes type to the shared RegexLike so the browser typechecks against the same structural matcher. * 🧹 refactor: Configure the file-config linear engine from a shared helper Move the RE2 wiring out of both JS server entry points into a single configureFileConfigRegexEngine helper exported from @librechat/api, so /api stays a thin caller and the setup no longer has to be kept in sync across index.js and experimental.js. Also warn loudly when compiling an endpoint's supportedMimeTypes drops every pattern (an empty allowlist would reject all uploads), and correct the isMimeTypeSupported docstring to say RegexLike rather than RegExp. * fix: fail closed when every MIME pattern fails to compile convertStringsToRegex returned [] when all configured patterns failed to compile, and filter.ts reads an empty allowlist as no restriction, so a restrictive config whose patterns all fail allowed every attachment. Return a single reject-all matcher instead so every consumer fails closed. |
||
|
|
dd159c4566
|
🔐 fix: Preserve Structured JWT Auth Context (#14652)
* 🔐 fix: Preserve structured JWT auth context
* fix: Omit identity from auth correlation logs
* fix: Isolate pre-auth request context
* style: Sort auth context imports
* fix: Preserve structured auth metadata
* fix: Narrow structured log formatter types
* fix: Annotate structured log context keys
* test: Fix request fixture typing
* fix: Namespace request path log context
* fix: Namespace request method log context
* fix: Preserve captured tenant error paths
* style: Sort tenant error imports
* fix: Classify bulk tenant isolation failures
* fix: Enforce safe request correlation invariants
|
||
|
|
45cc53c40b
|
🛰️ chore: bump @librechat/agents to v3.4.0, cover streamed subagent results e2e (#14647)
* test: cover streamed subagent results end to end * test: assert real e2e conversation id * test: harden streamed subagent e2e * test: stop incompatible subagent fixtures * chore: update @librechat/agents to version 3.4.0 in package.json and package-lock.json |
||
|
|
305107998d
|
🌊 fix: Preserve Custom Endpoint streamRate When endpoints.all Is Defined (#14645)
* 🌊 fix: Preserve Custom Endpoint `streamRate` When `endpoints.all` Is Defined `buildCustomOptions` assigned `allConfig.streamRate` unconditionally whenever an `endpoints.all` block existed, overwriting the per-endpoint `streamRate` with `undefined` for any `all` block that did not define one of its own. The value is read back later to set `_lc_stream_delay` on the llmConfig, so stream smoothing was silently disabled for every custom endpoint whenever `endpoints.all` was present for unrelated reasons (e.g. `activityLabel`). Guard on `allConfig?.streamRate`, matching the existing OpenAI path. * 🌊 fix: Preserve Explicit `streamRate: 0` Through the Custom Endpoint Chain Codex review: truthy guards dropped zero-valued streamRate at both the endpoints.all override and the llmConfig assignment. With agents 3.4.0 defaulting stream smoothing ON, 0 becomes the explicit disable, so both sites now use nullish guards; endpoints.all.streamRate: 0 overrides an endpoint-level rate and an endpoint-level 0 reaches _lc_stream_delay. Spec extended with both zero cases. |
||
|
|
56175af0b5
|
🎟️ fix: Reconcile MCP OAuth Readiness Across Pods (#14629)
* fix: stabilize MCP OAuth readiness across pods * fix: harden MCP readiness review findings * fix: resolve CI type check and terminal OAuth polling * fix: address MCP OAuth readiness review * fix: align MCP OAuth readiness state * test: stabilize MCP OAuth readiness assertion * fix: reject stale MCP OAuth callbacks * fix: close distributed MCP OAuth readiness gaps * style: sort Redis MCP test imports * fix: preserve MCP OAuth polling across rolling pods * fix: finalize distributed MCP OAuth readiness * fix: preserve runtime-detected MCP OAuth * fix: report runtime MCP OAuth readiness * fix: preserve live MCP OAuth classification * style: sort MCP connection imports |
||
|
|
9b7a30743b
|
🛂 fix: Honor Claude Document Limits Through Inference Gateways (#14650) | ||
|
|
489bc02d4a
|
🧭 fix: Fail Closed When Expected MCP Tools Are Unavailable (#14646)
* fix: fail closed when expected mcp tools are unavailable * test: strengthen MCP handoff coverage * fix: clarify unavailable MCP tool guidance * fix: preserve MCP discovery for empty catalogs |
||
|
|
7775f25b0d
|
🪢 fix: disable central fanout media uploads for langfuse (#14642)
* fix(langfuse): disable central fanout media uploads * test(langfuse): cover fanout media policy in run config * chore(deps): bump agents for Langfuse media policy * chore(deps): bump agents to 3.3.13 * fix(langfuse): gate central fanout media uploads |
||
|
|
4cec1a675f
|
🔒 feat: Add allowedAddresses Exemption to Speech (STT/TTS) and OCR Config Schemas (#14559)
* feat: add allowedAddresses exemption to speech (STT/TTS) and OCR config schemas Add the existing allowedAddressesSchema as an optional field on sttSchema, ttsSchema, and ocrSchema, reusing the schema already attached to endpoints, mcpSettings, and actions so port scoping and normalization stay identical. STT and TTS resolve a single provider by counting non-empty section keys, so exclude the allowedAddresses key from that scan. Without the exclusion a configured exemption list would be counted as a second provider and trip the "Multiple providers are set" guard. The field is inert on its own: nothing reads it for SSRF yet, and provider detection now ignores it. * fix: preserve allowedAddresses through the OCR config loaders loadOCRConfig rebuilt the ocr config with only apiKey, baseURL, mistralModel, and strategy, dropping allowedAddresses before it reached req.config.ocr. Pass it through in both the AppService loader (packages/data-schemas/src/app/ocr.ts) and the duplicate at packages/api/src/files/ocr.ts so the exemption survives config load. |
||
|
|
3f0a1ec8d9
|
🛡️ fix: Run message-filter PII patterns on a linear-time regex engine (ReDoS) (#14554)
* 🛡️ fix: Run message-filter PII patterns on a linear-time regex engine The messageFilter.pii middleware compiled admin-configured customPatterns with the native RegExp engine and ran them synchronously against every message on the shared event loop, so a catastrophic-backtracking pattern such as (a+)+$ could stall the entire process (native RegExp takes tens of seconds at roughly 32 characters) and take the instance down for every user. Compile these patterns with RE2JS, a linear-time RE2 port with no native addon, so catastrophic backtracking is impossible regardless of the pattern rather than something the code tries to detect. Patterns using features RE2 does not support, such as backreferences, fail to compile and are dropped and logged exactly as an invalid pattern already is. The filter only tests for a match, so this is a drop-in engine swap with no behavior change for valid patterns. * 🛡️ fix: Reject RE2-incompatible messageFilter patterns at config load The customPatterns regex was validated with native RegExp at config load, but the runtime now compiles it with a linear-time engine (RE2) that does not support backreferences or lookaround. Such a pattern passed validation, then failed to compile and was silently dropped at request time, quietly removing PII protection after upgrade. Reject backreferences and lookaround during config validation with an explicit message, and document RE2 syntax in the example config instead of "JavaScript-flavor". The runtime engine remains the authoritative boundary and still drops-and-logs anything this load-time check misses. * 🧹 test: Use direct MessageFilterPiiConfig annotations in the PII specs The added ReDoS cases satisfy the exported MessageFilterPiiConfig type directly, so the `as unknown as` assertions were unnecessary. Annotate the config objects directly, matching the repo's type-safety guidance. * 🧹 fix: Reject named backreferences in messageFilter patterns at config load Extend the config-load check to also reject named backreferences (\k<name>), which are valid JavaScript regex but unsupported by the linear-time runtime engine, so they surface at load rather than being dropped at request time. Together with the existing numeric-backreference and lookaround checks this covers the RE2-incompatible construct set; the runtime engine remains authoritative. * 🛡️ fix: Preserve Unicode whitespace matching in messageFilter starter patterns RE2's \s is ASCII-only, so after the engine swap the built-in api-key and Bearer starters no longer matched a secret separated by non-ASCII whitespace (e.g. a non-breaking space), which native RegExp did match. Broaden the whitespace classes to [\s\p{Zs}] so those patterns keep their original coverage, and add a regression test for a non-breaking-space separator. * 🛡️ fix: Validate messageFilter patterns with the RE2 engine at config load Replace the syntax blacklist (numeric/named backreferences, lookaround) with authoritative validation: config load now compiles each custom pattern with the same linear-time engine the runtime uses, so any RE2-incompatible construct (including control escapes like \cA) is rejected at load with a clear error instead of being silently dropped at request time. The validator is swappable and defaults to native RegExp so browser builds add no engine; the server wires the RE2-backed check at startup via configureMessageFilterRegexValidator in both entry points. * 🛡️ fix: Match the full whitespace set in messageFilter starter patterns RE2's `\s` omits the vertical tab and `\p{Zs}` omits U+2028, U+2029, and U+FEFF, so a separator built from one of those characters slipped past the `api-key` and `Bearer` starter patterns and reached the model. Broaden the starter whitespace class to the full JavaScript whitespace set so those separators are covered again. * fix: fail closed when messageFilter.pii compiles to zero patterns DB and admin config overrides bypass the RE2 schema validation (it only runs at YAML load), so an override whose only pattern is RE2-incompatible was dropped at compile time, left zero patterns, and let the request through. compile() now returns a failClosed flag when a config declared patterns but every one failed to compile; the middleware returns 400 and findPiiMatchInMessages returns a distinct misconfigured match that the OpenAI and Responses controllers surface with an admin-facing message. * 🛡️ fix: Fail closed when any messageFilter.pii custom pattern drops compile() previously set failClosed only when every pattern dropped (patterns.length === 0 && dropped > 0). With the default starters present, a single RE2-incompatible custom override incremented dropped but left patterns.length > 0, so the filter silently enforced only the surviving subset and text matching only the dropped rule passed. failClosed now keys off dropped > 0, so any dropped custom pattern blocks with the misconfigured 400. YAML patterns are RE2-validated at load, so dropped stays 0 for valid configs and only unvalidated DB or admin overrides can trip it. Reframed the two keeps-others-active specs to assert fail-closed and added a default-starters partial-drop regression. * 🧹 fix: Correct the misconfigured JSDoc and drop redundant casts in the PII specs The misconfigured flag now means any configured custom pattern failed to compile, not that every pattern failed, so its JSDoc on PiiMatch is updated to match. The partial-drop regressions now use direct MessageFilterPiiConfig annotations instead of as-unknown-as casts, keeping the specs type-checked, consistent with the rest of the suite. |
||
|
|
58cdd9cd8f
|
⚡ feat: Coalesce Redis Streaming Delta Publications into Windowed Batches (#14614)
* ⚡ feat: Coalesce Redis Streaming Delta Publications into Windowed Batches Every streamed delta currently costs two Redis EVALs (durable append + sequence-allocating publish), and the publish round trip is awaited inside the provider-stream consumption loop. Behind STREAM_DELTA_COALESCE_MS (default off), message/reasoning/run-step deltas now buffer for a small window and flush as one CHUNK_BATCH frame: a single INCRBY reserves consecutive per-event sequences and one EVAL publishes the batch, while a matching batched XADD keeps the durable chunk log on the same cadence so the resume frontier's log-vs-counter timing assumptions hold. Subscribers unpack batch frames at ingress into individually sequenced chunks, so the reorder buffer, duplicate drop, and force-flush behavior are unchanged. Durable, steer-receipt, created, and terminal emissions stay on the awaited per-event path and act as ordering barriers that flush any pending window first; terminal claims flush both sides before the status CAS so a warm tail cannot fence against its own completion. Benchmarked on local Redis (per-scenario RESETSTAT, INFO cpu/commandstats): at 100-200 ev/s a 25ms window cuts EVAL calls 67-82% and Redis engine CPU 52-70%; at the incident's 40 ev/s it halves EVALs while a 20ms window batches nothing (avg 1.0/frame). Producer await stall drops from ~0.9ms/delta to ~0.05ms/delta, matching the previously measured 16-18% USE_REDIS_STREAMS wall-time overhead. Delivery p95 stays under one window (27-28ms at 25ms). * 📝 docs: Document STREAM_DELTA_COALESCE_MS in .env.example * 🚧 fix: Drain Coalesced Windows Before Abort and Shutdown Terminal CAS abortJob and the graceful-shutdown finalizer claim terminal state through their own CAS calls rather than claimTerminalJob, so the pre-CAS coalescer flush did not cover them: a window tail buffered at abort time flushed against the already-aborted status, fenced (-1), and the false receipts retired the healthy runtime and error-closed subscribers before the abort FINAL frame. Extract the flush into flushCoalescedStreamBuffers and call it from all three terminal paths that can interrupt a live emitter (claim, abort, shutdown); the abort call sits ahead of the content snapshot so a chunk-log reconstruction also observes the flushed tail. Regression test aborts mid-window and asserts the tail is delivered with no subscriber error (fails without the fix). Paused-state terminals (approval expiry, pause-persistence timeout) need no flush: the pause's durable barrier already drained the window and nothing streams while paused. * 🛡️ fix: Keep Fence Retire a Lost-Signal Backstop on Aborted Runtimes A cross-replica abort claims its terminal CAS on the aborting replica, so the owner cannot drain its coalesced window pre-CAS; the window flush then fences against the aborted status. When the flush timer lands in the CAS-to-FINAL gap, the false receipts retired the owner runtime and detached its SSE handlers, so the abort FINAL published moments later was dropped and attached clients hung until client-side reconnect. The stop signal reaching the owner (~1ms pub/sub) is proof the abort/replacement flow owns terminal delivery and cleanup, so retireRuntimeAfterDurableFence now returns early for runtimes whose abort signal already landed. The forced teardown remains exactly for its original purpose: a fence observed by a NOT-yet-aborted owner, which is the lost-signal case. Regression test pins the race deterministically via the abort beforePublish hook (which runs between the CAS and the FINAL), forcing the owner flush there: without the guard the FINAL is dropped and the subscriber never completes; with it the FINAL delivers cleanly. * 🧰 fix: Gate, Isolate, and Bound the Coalesced Delta Path Three hardening fixes for the coalescing prototype. The manager now enables the fire-and-forget delta path only when the configured services actually batch — presence of flushPendingChunks/flushPendingAppends is the advertisement — so a custom transport that only implements emitChunk keeps the awaited per-event ordering contract even with STREAM_DELTA_COALESCE_MS set, and a batching transport is never paired with a per-event store (which would let the durable log trail the sequence counter by a full window). Batch unpack isolates each event: a throwing subscriber callback now degrades exactly like a lost individual frame (that sequence stalls until the reorder force-flush) instead of discarding the batch tail whose sequences were already reserved. And the emitter tracks outstanding coalesced receipts per stream, awaiting one once 256 accumulate: healthy settlement is a window plus a round trip so the count sits in single digits and the await never runs, while a stalled Redis now paces the producer exactly like the flag-off awaited path instead of accumulating batches, resolver closures, and queued commands without bound. Unit tests cover the capability gate (hint shape and await behavior for capable, incapable, and window-off configurations) and the backpressure threshold; an integration test pins the unpack isolation (fails without it: the batch tail vanishes instead of recovering via force-flush). Benchmark re-run confirms the counter and gate cost nothing measurable: identical EVAL counts and the serial drain still enqueue-bound. * 🎛️ fix: Make STREAM_DELTA_COALESCE_MS the Single Coalescing Switch The per-instance coalesceWindowMs constructor overrides could disagree with the environment the manager reads: overrides without the env silently did nothing, and an enabled env with an override of 0 selected the un-awaited manager path while both services published and appended per-event. Nothing in the repo passed these options, so remove them — the transport, the job store, and the manager now read STREAM_DELTA_COALESCE_MS through one resolver, making a half-enabled process unrepresentable rather than documented against. The capability-presence gate remains for services that do not implement batching at all. * 🧪 fix: Observe Abort Tail Delivery Before Terminal Teardown in Test The same-replica abort test waited for the coalesced tail only after abortJob returned, but abortJob's finally-block cleanup tears down local subscription state and publish receipts acknowledge Redis execution, not subscriber delivery. Single-node pub/sub delivers sub-millisecond so the frames always won locally; under the CI Redis Cluster they cross the cluster bus and lost the race, timing out the assertion. Await delivery concurrently with the abort instead — the pre-CAS flush publishes the tail several round trips before the teardown, so observing during the call is deterministic in both topologies. Test-only change. |
||
|
|
1bbe7dfe83
|
📦 chore: npm audit (#14640)
* chore: Update undici dependency to version 8.10.0 * chore: npm audit fix * chore: downgrade undici dependency to version 7.29.0 |
||
|
|
26ba2c2954
|
♿️ a11y: improve keyboard operability, focus retention, and accessible naming (#14600)
* fix: improve accessibility with semantic HTML and keyboard support * fix: preserve focus on attachments and stop CSS leaking into label text Passing `Wrapper` to FileRow as an inline arrow made it a new component type on every render, so React remounted the whole file row. A keyboard user who tabbed to an attachment thumbnail lost focus to <body> the moment the upload settled. Hoist the wrappers to module scope so their identity is stable. BlinkAnimation rendered a <style> tag into the DOM; stylesheet text becomes part of the ancestor's textContent and leaks raw CSS into label readouts. Move the keyframes into the tailwind config, named logo-blink to avoid colliding with the existing `blink` keyframes in style.css, and honour prefers-reduced-motion. * fix: make preset row actions reachable by keyboard The pin, edit and delete buttons on a preset row were hidden with `invisible`, which sets visibility: hidden and removes them from the tab order entirely. The `group-focus-within` variant meant to reveal them never fired, because nothing inside the row ever receives DOM focus during keyboard navigation. Verified in a browser: arrowing and tabbing through the presets menu skipped the row and the buttons reported focusable: false, while hovering made them focusable. Hide them with opacity instead, which keeps them in the tab order, and reveal on focus as well as hover. At rest they still compute to opacity 0, so there is no visual change. * fix: harden a11y heading, Space activation, and preset hit targets Gate the page heading on a title that matches the routed conversation so stale Recoil state is not announced during navigation. Ignore key-repeat on role=button TooltipAnchor activation while still blocking Space scroll. Disable pointer events on transparent preset actions until hover or focus. * fix: address a11y review follow-ups and eslint formatting Use the shared layout test harness for ChatView heading tests, default role=button TooltipAnchors into the tab order, ship spinner keyframes in package CSS, and let native preset buttons handle activation once. |
||
|
|
ccf4301093
|
🚦 feat: Configurable Circuit Breakers for Runaway Streamed Tool Args (#14613)
* 🚦 feat: Configurable Circuit Breakers for Runaway Streamed Tool Args * docs: forewarn create_file about the streamed tool-argument limit The breaker failing a near-limit write should not be the model's first exposure to the bound. Both create_file variants now state the default 64 KB per-call limit and the incremental pattern (create the first section, extend with edit_file) in the tool description and the content parameter description. * fix: keep skill create_file description under the provider advisory cap The limit-guidance paragraph pushed the skill-aware description to 1169 chars, past the 1024-char advisory bound where providers may truncate. The skill variant now carries the guidance only in its content parameter description, which sits closest to the generated payload and is not at truncation risk; the shorter code-sandbox variant keeps the full paragraph. * 🚦 feat: per-tool streamed-arg limits with a create_file default Thirty days of production data show create_file is the only tool class with legitimate near-limit arguments (p99 80.6 KiB; every other tool p99 under 10 KiB). Rather than loosening the global 64 KiB cap for all tools, the yaml gains maxToolCallArgBytesByTool (per-tool overrides, keyed by model-facing tool name, 0 disables that tool's guard) and LibreChat ships { create_file: 131072 } by default; yaml entries merge over and can replace it. Pairs with maxToolCallArgBytesByTool support in the agents SDK and stays inert until the dependency bump. * test: pass per-tool spec configs as plain Partial literals The as-TAgentsEndpoint casts fail TS2352 for object-valued fields: comparability does not grant nested literals the implicit index signature that plain assignability does, so casts carrying maxToolCallArgBytesByTool never sufficiently overlap. The mapper already accepts Partial<TAgentsEndpoint>, so the new cases pass uncast literals instead. * chore(deps): bump @librechat/agents to 3.3.12 |
||
|
|
b807292997
|
📉 perf: Bound Early Event Buffering for Detached Generations (#14612)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
* 📉 perf: Bound Early Event Buffering for Detached Generations
A generation streaming with no attached subscriber re-entered buffering
mode on every disconnect and retained each emitted event in
earlyEventBuffer for its remaining duration. A single 26-minute detached
run (~58,800 tool-argument deltas) grew the heap past 2 GiB with GC cost
climbing alongside it, while client reconnects always resume from
durable state and discard that local buffer anyway.
- Close the early buffer after the first attachment drains it in Redis
mode; the durable chunk log and pub/sub own recovery from then on,
matching how cross-replica subscribers already attach.
- Enforce hard bounds (5,000 events / 8 MB estimated) in both modes; on
overflow the buffer is discarded and closed, with recovery falling back
to the durable chunk log (Redis) or resume snapshot (in-memory).
- Add a generation_stream_early_buffer_overflows_total counter and
earlyBufferedEvents/Bytes gauges on getRuntimeStats() for visibility.
- Add incident-shaped regression tests and update specs that pinned the
old post-disconnect re-buffering contract.
* fix: redirect post-overflow first attachments to resume recovery
A buffer discarded by the overflow guard left the initial non-resume
SSE attachment with nothing to replay, silently omitting pre-attach
output until the final event. Track the overflow on the runtime and
close such attachments with the existing reconnect signal instead: the
client already re-attaches with resume=true on transport failure and
its sync frame reconstructs the discarded output from durable/snapshot
state. Adds no per-event work; the check is one boolean per attachment.
* fix: enforce buffer bounds when restoring canceled resume captures
Captured emissions restored by a resume canceled before activation
bypassed the early-buffer hard cap, so one oversized restoration could
persist past the limits with no later emission to trip the guard.
Restoration now applies the same overflow-and-close behavior through a
shared helper, and the restore-cap spec fails before this change
(5 events / ~10MB retained) and passes after.
* chore: add Redis management scripts and update package.json for Redis commands
|
||
|
|
120ee2afa6
|
🚦 fix: Bound, Single-Flight, and Retry Skill File Priming Uploads (#14611)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
Skill priming fanned out one unbounded batch upload per cold skill, bursting through codeapi's per-user upload limiter (30 per 5 min). Failures degraded silently: nothing persisted, every turn re-burned budget, and handle_skill reported success with no files mounted. - Bound batch uploads to 3 process-wide slots across both prime paths - Single-flight primeSkillFiles per (skill id, version) - Retry a 429 once per Retry-After, capped at 15s, fresh streams - handle_skill now tells the model when bundled files are unavailable - Warn on fulfilled-null primes in primeInvokedSkills |
||
|
|
b11978017d
|
🧱 fix: Enforce Agent Runtime File Trust Boundaries (#14577)
* fix: secure agent runtime file metadata * chore: sort agent resource test imports * fix: Align Agent Tool Resource Types * fix: Rehydrate Agent Image Resources * fix: preserve remote agent file authorization |
||
|
|
178e61b763
|
⚓ fix: Bind Action Servers to Metadata Ports (#14575)
* fix: bind action server ports * style: sort action imports * fix: normalize action port input * fix: parse action ports consistently |
||
|
|
db6ba5392a
|
🪢 fix: Bind MCP OAuth Secrets to Trusted Endpoints (#14578)
* fix: bind MCP OAuth secrets to trusted endpoints * fix: bind stored MCP OAuth clients during refresh * fix: address MCP OAuth review findings * fix: bind stored MCP OAuth credentials * fix: make MCP OAuth credentials generation-safe * test: update MCP OAuth uninstall binding fixtures * fix: harden MCP OAuth credential persistence * fix: scope MCP OAuth refresh single-flight * style: sort MCP OAuth token imports |
||
|
|
cdf437dc5b
|
🪺 fix: Keep Preempt-Abandoned Siblings Nested, Make Message Tree Order-Robust (#14587)
* 🪺 fix: Keep Preempt-Abandoned Siblings Nested, Make Message Tree Order-Robust * 🔗 fix: Sever Cycle Back-Edges So the Returned Message Tree Is Acyclic * 🧪 fix: Satisfy TFile in buildTree Spec fileMap Fixture * 🌲 fix: Assert Repaired Trees in convoStructure Specs, Uncharge Self-Parent Edges * 📌 feat: Identity-Stable Sibling Selection Across Background Tree Churn * 🎭 test: E2E Coverage for Thread Fold and Sibling Selection Invariants * 🔑 fix: Treat Newest-Sibling Re-Key as Hydration, Not a New Branch * 🧭 fix: Rebind Sibling Selection Per Parent, Detect Appends by Membership |
||
|
|
928b14f5bc
|
🔒 fix: Single-Flight MCP OAuth Token Refresh per User/Server (#14596)
* 🔒 fix: Single-Flight MCP OAuth Token Refresh per User/Server Concurrent refresh-token redemptions (tool-call 401, ping, reconnect retries, expired-token reads) each replayed the same stored refresh token at the OAuth token endpoint. RFC 9700 reuse detection treats the replay as theft and revokes the entire grant family, forcing manual re-consent every access-token expiry. MCPTokenStorage.forceRefreshTokens is the choke point every refresh path converges on; it now single-flights redemptions per (tenantId, userId, serverName) so concurrent callers share one wire call and receive the same rotated result. The refresh token is re-read from storage inside the locked execution — never from a caller snapshot — so a redemption starting after another refresh completed uses the rotated token instead of replaying the consumed one. Fixes #14583 * 🧪 test: Isolate Single-Flight Keys per Test via Unique Server Names * 🔒 fix: Evict Stalled Refresh Slots, Decouple Waiter Aborts from Shared Redemption Codex review round 1: - A redemption that never settles no longer wedges the single-flight slot until process restart: a stale-entry timer evicts the map entry so later refreshes start fresh, while existing waiters keep their promise. - Caller AbortSignals no longer thread into the shared redemption. An impatient waiter (silent refresh's short timeout) resolves its own wait with null via a per-waiter race; the shared wire call proceeds for everyone else, bounded by transport timeouts plus eviction. * 🔒 fix: Abort Stalled Refreshes Before Slot Release, Hook Cache Invalidation to Redemption Codex review round 2: - The stale timer now aborts the wedged execution instead of deleting its slot; the slot frees only once the execution has settled, and an abort guard before the token-endpoint call stops a woken pre-wire stall from replaying a refresh token a successor already rotated. - New onRefreshSuccess hook runs inside the shared redemption after rotated tokens persist, so the silent-refresh path's mcp_get_tokens cache invalidation fires even when the initiating waiter timed out before the redemption completed. * 📝 docs: Record Post-Dispatch Abort Recovery Rationale on Stale-Refresh Valve |
||
|
|
7e74f8eb8c
|
🪪 fix: Strip Unresolved Header Placeholders at Final Resolution (#14595)
Unresolved {{LIBRECHAT_USER_*}} header templates leaked literally to
upstream providers when user context was missing at resolution time
(e.g. async title generation racing client disposal), letting a gateway
trust LibreChat's own template syntax as an account identity.
resolveHeaders now takes an opt-in stripUnresolved flag that blanks any
resolvable-but-unresolved LIBRECHAT_USER/BODY/OPENID placeholder, enabled
at every final resolution boundary (resolveConfigHeaders, model fetches,
Google init, summarization overrides, azureAssistants init). Staged
passes that resolve again later with more context are left untouched, as
is the async-resolved {{LIBRECHAT_GRAPH_ACCESS_TOKEN}} and unknown names.
titleConvo now resolves headers from the req captured at entry instead of
re-reading this.options.req, which disposeClient nulls concurrently.
Fixes #14580
|
||
|
|
59395a6bf0
|
🪢 refactor: Move Agent Execution Seam Before Initialization (#14581)
* refactor: move agent execution seam before initialization * refactor: type librechat agent request extensions * refactor: read envelope values from descriptors * fix: preserve envelope types and validation errors * fix: bound agent envelope traversal |
||
|
|
ad0f72dede
|
🌀 ci: Deterministic Circular Dependency Checks (#14579)
* 🌀 ci: Deterministic Circular Dependency Checks * 🌀 ci: Enforce Type-Level Edges in Circular Dependency Scan * 🌀 ci: Materialize Import-Type Expression Edges in Cycle Scan * 🌀 ci: Collect Inline Type-Only Specifier Edges in Cycle Scan |
||
|
|
b253b623fe
|
📦 chore: update sanitize-html to latest (#14573)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 📦 chore: update `sanitize-html` to latest
* chore: add additional modules to esModules for Jest configuration
|
||
|
|
9fbea04d46
|
🦗 fix: Deliver Abort Acknowledgements on Zero-Subscriber Replicas (#14569) | ||
|
|
6f45a9e32e
|
🔗 fix: Normalize MCP Tool Keys at Every Producer, Resolve Raw Names via Aliases (#14553)
* 🔗 fix: Normalize MCP Tool Keys at Every Producer, Resolve Raw Names via Aliases Tool keys had two spellings that could diverge for any server whose name contains characters outside [a-zA-Z0-9_.-]: the tool cache (and registry inspector) built keys with the RAW server name, while runtime instances are named with normalizeServerName(serverName). Three code comments already asserted "tool keys embed the normalized server name" - no producer honored it. For a special-character server that meant: - definitions-only mode shipped raw def names the model echoed back, but the executor's tool map held the normalized instance name, so every call failed with "Tool not found"; - per-tool tool_options (defer_loading / allowed_callers / run_in_background / describe_intent) were persisted under raw keys that never matched the definition names the option passes resolve against, so builder settings were silently inert; - tool-key parsing against normalized candidate lists silently fell back to last-delimiter splitting, which mis-parses delimiter-bearing tool names. The reconciliation is one contract enforced in three moves: 1. PRODUCERS NORMALIZE. The tool cache (packages/api/src/mcp/tools.ts) and the registry inspector build keys with the normalized server name, matching the instance names MCP.js has always assigned. The builder's tool ids, agent.tools entries, tool_options keys, and definition names all flow from these keys, so every model-facing name now agrees. The cache STORE stays keyed by the raw config name. 2. CONFIG LOOKUPS RESOLVE ALIASES. New shared helpers in data-provider (buildServerNameAliases, normalizeMCPToolKey) map a parsed normalized name back to the raw config name that the registry, config maps, tool cache, and plugin-auth rows are keyed by. Applied in the definitions loader closure, handleTools grouping, createMCPTool's parsing fallback, getUserMCPAuthMap, and the MCP tools endpoint - matching both spellings so legacy raw keys keep resolving. 3. LEGACY DATA HEALS AT ONE BOUNDARY. initializeAgent rewrites raw-keyed agent.tools entries and tool_options keys to the normalized form (normalizeAgentToolKeys) before anything consumes them, so agents persisted under the old convention load their tools AND have all four per-tool options honored. Placeholder and server-pin tokens stay raw - they are config-identity references, not model-facing names. Servers whose names are already in the safe character set (the common case) produce byte-identical keys before and after; the fast path allocates nothing. Stale Redis-cached raw keys self-heal via the existing reconnect-on-missing path within one cache cycle. * 🧯 fix: Deterministic Alias Collisions + Raw Names in Definition Metadata Two review findings on the normalization contract: - Two configured server names that normalize to the same segment (e.g. 'Sales Force' and 'Sales:Force' -> 'Sales_Force') produce inherently ambiguous tool keys; the alias map silently resolved last-wins, so a tool selected from one server could execute against the other's config. buildServerNameAliases now resolves collisions to the FIRST configured name deterministically, and resolveMCPServerContext warns once per colliding pair per process so the operator can rename one server. A collision-resistant identifier would change every existing tool key, so detection + stable routing is the right treatment here; startup-time config validation can follow separately. - The definitions loader resolved parsed (normalized) server names to raw only inside the ToolService closure, while the definition metadata (serverName -> mcpRawServerName) kept the normalized value. Server instructions are keyed by raw config names, so a special-character server's instructions were silently omitted in definitions-only mode. loadToolDefinitions now takes rawServerNames, resolves the boundary against both spellings, and stores the RAW name in definition metadata - consistent with the instance path. * 🧯 fix: Heal Stale Caches, Skill Allowed-Tools, and Builder Selectors Three review findings on the normalization rollout, all in the transition class: - Stale cache entries (P1): the definitions-only loader treats the per-server tool map as authoritative and never reconnects on a per-key miss, so a pre-change raw-keyed Redis entry would make a special-character server's tools vanish for up to the cache TTL. getMCPServerTools now heals legacy raw-keyed entries to the normalized format at read time (keys and function names), covering every consumer with no coordinated invalidation; safe names return the map untouched. - Skill allowed-tools: a skill declaring a raw MCP key in allowed-tools bypassed the initialize-boundary heal (the union runs after it) and would neither dedupe against healed agent tools nor match the normalized tool map. The primes' allowedTools now pass through the same normalizeAgentToolKeys heal before unioning. - Builder selectors: matchesMcpServer and useVisibleTools parsed tool ids against raw server names only, so an attached special-character server rendered as an unselected orphan card. Both now accept the normalized spelling and resolve it back to the raw map key, keeping legacy raw ids working. * 🧯 fix: Fail Closed on Normalized Server-Name Collisions Escalation of the collision finding: a deterministic first-wins alias plus a warning still let the tools listing publish BOTH colliding servers, so a tool selected under the shadowed second server would silently execute against the first server's configuration (their model-facing keys are identical, so routing cannot ever distinguish them). - findShadowedServerNames identifies later-configured names whose normalized form an earlier different name claimed. - getMCPTools excludes shadowed servers from the published listing entirely (with a warn naming the collision), so their tools are never selectable - nothing ambiguous can be picked. - Server creation reserves both spellings: a generated slug may not collide with a raw config name OR the normalized form its tool keys would carry. Collision-resistant model-facing IDs remain out of scope: changing normalizeServerName's output would rewrite every existing tool key (agent documents, caches, instance names) for ALL servers to handle a misconfiguration that is now blocked from exposure instead. * ✅ fix: Dedupe Reserved Server-Name Spellings at Creation The reservation list appended normalized forms unconditionally, which duplicated every safe name (raw === normalized) and broke the route-level contract test pinning the exact list. Dedupe via a Set so safe names contribute one entry, while special-character names still reserve both spellings; adds the special-character reservation case. * 🧯 fix: Never Heal a Shadowed Server's Keys; Align Authorization Tie-Break Persisted references were the remaining collision vector: an agent or skill saved with the shadowed later server's raw key was HEALED into the shared normalized key, authorized through a last-wins map, and routed first-wins - authorized as one server, executed as another. - normalizeAgentToolKeys now refuses to rewrite keys of shadowed servers (findShadowedServerNames): rewriting would produce exactly the first server's key. Left raw, the key cannot match the normalized-keyed tool map and the tool fails visibly - broken beats misrouted. Covers agent.tools, tool_options, and skill allowed-tools through the shared heal. - filterAuthorizedTools (agents/v1.js) builds its normalized-to-raw map via the shared buildServerNameAliases instead of a last-wins Map constructor, so authorization resolves a colliding key to the SAME first server execution routes to. * 🧯 fix: Direct Identity Wins Over Aliases; Heal Client Forms and Degraded Contexts Four review findings on the normalization edges: - Alias hijack (P1): a user-DB server named exactly like an operator server's normalized form ('foo' vs YAML 'foo!') had its tools rerouted to the operator server by unconditional alias resolution. Resolution is now DIRECT-FIRST everywhere: the parsed name is tried as-is, and only when nothing resolves is it treated as a normalized spelling (definitions loader, handleTools grouping, createMCPTool fallback). buildServerNameAliases seats identity entries before derived ones so a literal name owns its slot regardless of config order, findShadowedServerNames and the collision warning derive from the same construction, and getUserMCPAuthMap fetches auth under both spellings so either owner finds its rows. - Builder double-match: a normalized name containing the delimiter ('foo mcp bar' -> 'foo_mcp_bar') also suffix-matched a server named 'bar', selecting both cards and making removal strip the wrong tool. matchesMcpServer now resolves the token ONCE against the full configured list (longest boundary, both spellings) when the caller supplies it; selection and removal share the resolution. - Builder legacy ids: an agent saved with raw-keyed ids showed its tools unchecked while the runtime heal kept them active, and selection updates never replaced the legacy entries. McpSection maps legacy raw ids to their current normalized ids when deriving and rewriting this server's selection. - Degraded context: a transient ensureConfigServers failure returned an entirely empty context, leaving normalized keys unresolvable for the request. resolveMCPServerContext now keeps the name lists (they derive from the config snapshot alone) and degrades only the lazy-init configs. * 🧯 fix: Collision Detection Sees Accessible Servers; Shadowed Refs Fail Closed End to End Round follow-ups on the collision design, all in the DB-server-visibility class: - The legacy-key heal detected collisions against operator-config names only, so healing could still produce a key that direct-first resolution routes to an invisible user-DB server. initializeAgent gains an optional getAccessibleMcpServerNames dep (wired through ToolService for controllers that mock it, directly elsewhere), consulted ONLY when a configured name needs normalization - zero cost for safe-name deployments. The heal then sees the full accessible set and skips shadowed servers' keys. - Wildcard and legacy raw tokens bypassed catalog filtering, letting a shadowed server's instances join a run under the same normalized names as the winner's. filterAuthorizedTools rejects tools of shadowed servers at authorization (its merged map sees DB + config), and handleTools skips them at execution. - The builder migrated only tool selection, not tool_options: legacy raw option keys showed disabled while the runtime honored them, and toggles could not clear them. McpSection now migrates option keys to the current normalized ids (existing normalized entries win). - A transient ensureConfigServers failure degraded to an EMPTY server context, leaving normalized keys unresolvable for the request. resolveMCPServerContext keeps the name lists (derived from the config snapshot alone) and degrades only the lazy-init configs. * 🧯 fix: Complete the Collision Audit at Every Gate; Safer Heal Semantics Round follow-ups hardening the collision audit: - Execution guards now consult the FULL accessible set: the caller's heal threads its already-fetched names through loadTools, and handleTools fetches them itself when a configured name needs normalization (never for safe-name deployments) - so a cross-tier collision (user-DB 'foo' vs operator 'foo!') fails closed at eager execution instead of joining the run under one normalized name. - Healing is SKIPPED when the collision audit cannot complete (transient lookup failure, or no dep): un-healed raw keys still resolve through the direct-first candidates, so skipping is safe while rewriting against an incomplete audit is not. - The audit lookup is gated on the agent actually carrying delimiter-bearing keys (tools, tool_options, or skill allowed-tools), so non-MCP agents never pay a registry round-trip even on specially named deployments. - normalizeAgentToolKeys gives the CURRENT (normalized) entry precedence when both spellings carry options, matching the builder's migration semantics instead of letting insertion order decide. - The builder's toCurrentToolId resolves entries boundary-exactly against every configured server (longest match, both spellings), so a raw suffix shared with a LONGER server name can no longer reassign that server's selection or options while another dialog is open. * 🧯 fix: Shared Collision Audit for Definitions Loading; Fail Closed on Audit Failure Round follow-ups closing the remaining audit gaps: - The definitions-only loader now consumes the same collision audit as eager loading: shadowed servers' entries (wildcards included) are dropped before definitions are emitted, so the default execution path can never resolve a shadowed server's normalized function name to another server. The audit names thread from initializeAgent's heal; the loader self-fetches only when a configured name needs normalization. - resolveCollisionAuditNames centralizes the audit-resolution policy (threaded set > self-fetch when needed > incomplete on failure), and BOTH loaders now fail closed under an incomplete audit: any normalization-sensitive reference (its own name needs normalizing, or it equals the normalized form of a configured special-character name) is skipped with a warning instead of being audited against operator names alone. isNormalizationSensitiveName lives in packages/api as a pure helper so test mocks use the real predicate. - normalizeAgentToolKeys collapses duplicate ids after healing (order-preserving): a document carrying both spellings converges on one key, never two instances with the same function name. * 🧯 fix: Thread the Audit Everywhere; Identity-Aware Alias Fallback Round follow-ups on audit plumbing: - The OpenAI-compatible and Responses tool loaders now forward the already-resolved accessibleMcpServerNames instead of discarding it, so the definitions loader neither repeats the registry lookup nor fails closed on a transient second lookup after the first succeeded. - The skill-only path threads its audit: when the baseline agent has no MCP keys but a primed skill's allowed-tools fetched the complete set, that set (not the operator-only list) reaches the loader, so the collision remains visible and the shadowed reference stays rejected end to end. - OAuth discovery iterates the collision-FILTERED tool list, so a request can no longer emit an OAuth prompt, wait out the connection timeout, and reconnect a server whose definitions were deliberately rejected. - The definitions loader's alias fallback is identity-aware: when the parsed name IS a known accessible server, a null tool fetch means temporarily unavailable (OAuth pending, missing user variables, disconnected) and no longer reroutes to the raw alias - previously the aliased operator server's definitions could be emitted under the unavailable DB server's names. * 🧯 fix: Legacy-Key Definition Lookup; Retain Audit for Deferred Execution - createMCPTool resolves tool definitions by BOTH spellings: the key as persisted plus the canonical normalized key built from the resolved server name. Assistants and direct tool calls persisted before the rollout bypass the agent-boundary heal and arrive with raw keys, while availableTools is now indexed canonically - previously every such call missed the index, burned a reconnect, and returned the unavailable stub permanently via the negative cache. - The initialized agent retains accessibleMcpServerNames (the COMPLETE collision audit this initialization resolved), buildAgentToolContext copies it into every per-agent tool context, and loadToolsForExecution threads it into the eager loader as bare options. Deferred/event-driven execution therefore reuses the snapshot instead of repeating the merged registry read - a transient failure there could fail-closed a tool the same turn already advertised from the successful first audit. - MCP.spec.js keeps @librechat/api pure helpers REAL (requireActual spread) so normalization paths are exercised rather than mirrored. * 🧯 fix: Parse Legacy Keys Against Both Server-Name Spellings createMCPTool's boundary candidates were normalized-only, so a legacy raw key whose server name contains the delimiter (foo_mcp_bar!) missed the suffix match and fell to the generic last-delimiter split - the canonical rebuild then produced a key that could never hit the index and the persisted call stubbed out. The candidate list now carries the RAW resolved name (and raw config names on the parse-only path) next to the normalized spellings. * 🧯 fix: Honest Audit Completeness; Shadowed-Server Form-Key Guard - resolveAllMcpConfigs tolerates ensureConfigServers failures, so the merged registry read can silently omit config-only servers while the audit still reported complete: true - a foo/foo! collision would go unseen and a persisted key could route to the wrong server. Both audit consumers now union the snapshot-derived raw config names back in (resolveCollisionAuditNames unions the caller's rawServerNames; the initializeAgent heal unions configRawServerNames), keeping the completeness label honest without an extra read: operator names come from the registry-independent config snapshot, user-DB names from the merged read that fails loudly into the existing incomplete path. - The client tool_options migration now mirrors the runtime heal's fail-closed rule for SHADOWED servers: when the dialog's server has lost its normalized slot to another catalog name, legacy raw keys stay raw instead of being rewritten onto the winning server's key, where a later save would apply the wrong server's per-tool settings. The dialog's own server joins the alias construction so a stale catalog map can't misread as a collision. * 🧯 fix: Heal Legacy Assistant MCP Tool Names on Save The assistants create/update controllers look tools up in the cached definitions by exact key, and the cache is now normalized-keyed - an assistant saved before the convention resubmits its raw-suffixed MCP name on every edit, so any save silently removed the tool. healMcpToolNames pre-heals the payload's tool list: a delimiter-bearing string that misses the cache resolves through the configured raw names (longest-suffix, boundary-exact) and rewrites to the normalized key only when that key actually exists in the cache. SHADOWED raw names stay raw and fail closed, mirroring the runtime heal; the config read happens only when a delimiter-bearing name actually misses, and read failures propagate (write path) rather than silently dropping tools. v2's update loop also stops re-reading the tool cache per iteration. * 🧯 fix: Full-Audit Shadow Set + Dedupe in the Assistant Key Heal - The assistant-save heal built its shadow set from operator config names alone, so a cross-tier collision (user-DB `foo` owning the normalized slot of operator `foo!`) looked unshadowed and the legacy key healed into the shared normalized name - which direct-first execution then binds to the DB server. The shadow set now comes from resolveCollisionAuditNames' full accessible audit, and an incomplete audit skips healing outright (every rewrite candidate is normalization-sensitive by construction, so raw-and-fail-closed is the only safe answer). - Healed string entries dedupe order-preserving: a payload carrying both spellings of the same tool collapses to one entry instead of expanding into duplicate function definitions the provider rejects. |
||
|
|
de033b7dbd
|
🦗 fix: Ignore Sequenced Redis Events Without SSE Subscribers (#14557) | ||
|
|
b9ca391b84
|
📦 chore: bump @librechat/agents to v3.3.11 (#14562)
|
||
|
|
e7f1838515
|
⚡ feat: Reliable Interrupt & Steer Escalation and Recovery (#14558)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* feat: surface interrupt-steer escalation on waiting messages The interrupt & steer feature shipped reachable only through the composer chord, the send-button hovercard, and the composer button; a message already waiting (queued for after the run, or steered and parked at the next tool boundary) had no path to it. Both waiting surfaces now carry one: - Queued rows get an icon-only ZapOff escalation button beside the existing Steer primary. It routes through sendQueuedNow, which now takes a preempt option on its live-run path. The tooltip teaches the composer chord, derived through resolveComposerKeyDown so a rebound or yielded chord is never advertised. - In-flight steer bubbles get an "Interrupt now" overflow entry with the same race rules as Edit: reclaim first, and only a `reclaimed` outcome resubmits (via retrySteer with preempt, swapping the chip for an interrupting one). `applied` and run-ended-mid-reclaim outcomes stop at the existing informational toasts, so the words can never land twice. Not offered on a steer already preempting. - Every during-run overflow menu gains an "Always interrupt instead" toggle for steerInterruptsByDefault, next to the existing steer/queue default toggle. MenuEntry supports disabled for the new entries. Only one interrupt can be unresolved at a time: while one preempt is pending (or the run is paused on approval, where the server 409s), every escalation control disables instead of racing the same seal. Ten new tests across both surfaces; 381 green in the affected suites. * fix: lock escalation across its reclaim window, keep the paused control visible, label as steer Codex round 1, all three findings. P2, escalation race. The single-interrupt invariant had a window between clicking "Interrupt now" and the reclaim resolving, where no preempt chip existed for the chip-derived gate to see: two bubbles escalated back-to-back could both resubmit. A shared escalating flag (Jotai, per-conversation) now covers the window and disables every escalation control on both surfaces, and a fresh recheck before resubmitting catches an interrupt armed elsewhere meanwhile (composer chord, queued row); those words re-home to the queue with an informational toast instead of breaking the invariant. P2, unreachable paused state. canSteer is defined as hasRealConvoId && !pausedOnApproval, so gating the button on canSteer removed it exactly when it was meant to render disabled; the test only passed on an impossible stub combination. The render gate is now duringRunActive && (canSteer || pausedOnApproval), and the test uses the real invariant. P2, label semantics. "Interrupt & send now" borrowed the name of the hard-abort action; this one preserves the partial answer and steers. Renamed to "Interrupt & steer now" (com_ui_interrupt_steer_now). Both behavior fixes counterfactually verified; 384 tests green across the affected suites. * fix: disable bubble escalation while the run cannot accept a steer Codex round 2, one P2. Answer mode (ask_user_question) sets duringRunActive false while pausedOnApproval stays false, since that flag only detects approval-bearing tool calls. The bubble's escalation entry stayed enabled there, so clicking it cancelled a healthy waiting steer and the preempt resubmission bounced off RUN_PAUSED, degrading the words to the queue. The entry now also disables on !duringRunActive, matching the queued-row control's gate. Counterfactually verified: reverting the gate fails the new answer-mode test. * fix: recheck live run state after the reclaim, not just at the click Codex round 3, one P2, and it is the round-1 recheck principle applied one level deeper: the entry-time disable cannot see a run that pauses (tool approval, answer mode) while the reclaim round-trip is in flight, and the .then closure held the render's stale steering controls, so the resubmit would fire into a RUN_PAUSED rejection after the reclaim had already surrendered the steer's boundary slot. The escalation continuation now reads the LIVE controls through a latest-ref: if the run can no longer accept a steer, the words re-home to the queue with an informational toast instead of resubmitting, and the resubmit itself also goes through the live controls. Counterfactually verified: reading the stale closure instead of the ref fails the new mid-reclaim pause test. * refactor: make escalation one atomic server-side arm, in place Codex round 4: four P2s, every one an interleaving of the same window — escalation as reclaim-then-repost is a compound, non-atomic operation whose continuation must revalidate the world (FIFO position lost, ref assigned too late, no run fence, competing bubble actions). Rounds 1-3 patched that window with a lock and rechecks; round 4 shows the window itself is the defect, so this removes it instead of guarding it again. Escalation is now POST /chat/steer/arm: the server flips preempt on the EXISTING queued item in one atomic store op (new IJobStore.armSteer; a decode-patch-encode LSET Lua on Redis, an in-place mutation in memory), fenced to the validated generation and refused once the queue closes. The handler mirrors the steer POST's preempt contract exactly: durable flag gated on the owner's recorded capability, volatile requestPreempt fire-and-forget because the durable flag is the truth resume/handover re-arm from. By construction this resolves all four findings: FIFO survives (the item never moves; the whole queue still drains in instruction order at the seal), there is no continuation to hold stale controls, the store op is fenced to the original run, and a competing Edit/Queue/Cancel either beats the arm (armed:false, chip untouched) or operates on the armed item, whose cancel already disarms. The client escalation entry becomes one mutation: armed:true relabels the chip in place (same steerId, same position), PREEMPT_UNSUPPORTED and lost races toast honestly, and the round 1-3 machinery — the escalating lock atom, the latest-ref, the post-reclaim rechecks and their two toast strings — is deleted rather than extended. Verified: 7 new handler tests on the real in-memory manager (including FIFO preservation and the stale-generation fence), 2 Redis integration tests against real Redis (in-place arm keeps order and every field; missing/stale/closed all refuse), client suites 396 green. * fix: decide capability inside the atomic arm, neutralize the lost-race toast Codex round 5, both findings, both edges of the new arm design rather than its mechanism. P2, capability TOCTOU. A HITL resume on a rolling deploy rewrites preemptCapable for the SAME generation, so the handler's read could go stale between validation and the flag flip, arming a steer the live owner cannot seal. armSteer now returns armed | missing | incapable, with the owner's live capability part of the same atomic predicate as the generation fence (HGET preemptCapable inside the Lua; the flat job field, not a metadata blob — the in-memory store reads the same field). The handler's pre-check is deleted rather than kept alongside; the store predicate is the single source. New handler test rewrites the capability after queueing and expects PREEMPT_UNSUPPORTED with the item left unflagged; the Redis guards test now asserts the incapable refusal against real Redis. P2, ambiguous toast. armed:false covers injected, cancelled, re-homed, and run-over alike, so telling the user the message "already reached the agent" claimed one specific outcome. The lost-race branch now uses a neutral message (com_ui_steer_arm_lost_race) and defers to the events for what actually happened. * fix: flip the escalation lock synchronously before the arm request Codex round 6, one P2. Round 4 deleted the escalating flag along with the reclaim continuation it guarded, but that left the one-interrupt gate blind during the arm request's own round trip: the chip-derived check cannot see an arm until its response relabels the chip, so on a slow connection two bubbles could both arm before either response landed. Double-arm is harmless server-side now (the run seals once and drains the whole queue in order), but every escalation control advertises "one interrupt at a time" by disabling, and the controls must tell the truth. The per-conversation escalating flag returns as a pure UX gate: set synchronously at click, before the mutation, cleared on settlement, and folded into interruptPending on both surfaces. Unlike its round 1-3 ancestor there is no continuation behind it to guard and no recheck to pair with it. Counterfactually verified: without the synchronous set, the two-bubble race test arms twice. 207 tests green across the Chat Input suites. * test(e2e): cover escalation of waiting messages through the real seal Three mock-harness tests on E2E_SLOW_REPLY, a 160-chunk stream with no tool boundary, so an in-thread steer part can ONLY come from a genuine mid-stream seal — which makes each test a behavioral proof rather than a UI check: - Queued row escalation: the ZapOff button turns a waiting queued message into a preempt-armed steer (202 echoes preempt: true) that seals and injects, where the sibling steering.spec test proves the unescalated path waits for run end instead. - Bubble in-place arm: an ordinary steer (202 with no preempt echo) waits as a bubble, POST /chat/steer/arm answers armed: true, the bubble relabels in place (same single bubble, same text, escalation no longer offered on reopen), and the armed steer seals mid-stream. - Always-interrupt toggle: flipped from a waiting row's overflow menu, plain Enter now produces a preempt: true steer that seals in the SAME run, and the menu offers the way back. An afterEach clears the localStorage preference so a mid-test failure cannot leak preempt-by-default into the rest of the serial suite. All three verified locally through the full harness (real backend, mock LLM, seeded DB): 3 passed in 27s. * feat: dedicated escalation arrow + shortcut, menu split into actions and preferences The escalation was still half-hidden: the bubble only offered it inside the overflow menu, and the tooltip taught the composer chord, which does a different thing (interrupts with typed text, not this chip). Three changes make it a first-class command: - A shared EscalateNowButton (circular arrow, ghost-bordered like the composer's interrupt control) is always visible on BOTH surfaces: beside each queued row's Steer primary and on every waiting steer bubble next to its menu. It disappears once a steer is interrupting. - A dedicated registry shortcut, escalateSteer (Cmd/Ctrl+Shift+.), editing-allowed and rebindable like every other action. Deliberately NOT an Enter chord: the composer owns every Enter chord, and the yield design rests on no default binding using Enter besides submit. Its handler clicks the newest enabled arrow control (bubbles beat queued rows), so the shortcut can never diverge from the button, and the arrow's tooltip teaches THIS command via the registry display. - The overflow menus separate one-off actions from sticky behavior changes: Edit, Cancel, Queue, then a smaller "Preferences" section holding the queueing and always-interrupt toggles, each with the standard InfoHoverCard reusing the Settings panel's descriptions. "Interrupt & steer now" leaves the menu entirely. 386 client tests green, including a menu-structure test locking the order and the absence of the escalation entry; bubble escalation tests drive the visible arrow. The e2e spec's bubble test now clicks the arrow, and a fourth test drives the dedicated shortcut end to end through a real mid-stream seal. * style: bind the escalation arrow to its message (variant A anatomy) Two same-weight circles in a row read as one control group, leaving the arrow's ownership ambiguous, and a floating arrow stops meaning anything once several messages stack. The shared control now carries variant A's anatomy: a thin divider binds a small SOLID arrow (filled, inverted) to the message region on its left, and the menu ellipsis stays a bare glyph, so the two affordances can no longer blur together — and the divider+arrow pairing repeats cleanly per chip at N messages. * chore: drop the unused within import CI lint caught * fix: advertise the escalation shortcut only while the control is live Codex on the e2e head, one P2: the tooltip appended the chord hint even while the button was disabled, advertising a shortcut that does nothing during an approval pause. The flagged control (InterruptNowButton) was since replaced by the shared EscalateNowButton, which inherited the pattern; the successor now omits the chord whenever the control is disabled, matching the rule the during-run hovercard already follows. * fix: harden steer escalation lifecycle and recovery * test(e2e): disambiguate accessible steer preferences * test: align abort persistence coverage with prerequisites * chore(i18n): remove obsolete steer race message * chore: normalize imports across steering changes * test: exercise stream integration on Redis Cluster * test: scope HITL checkpoints to generation * test: fix cluster cleanup and locale policy * fix: keep escalation visible during ask pauses * fix: fence recovery downgrade and stale predecessors * fix: require generation owner abort acknowledgement * fix: validate delayed preempt arms * test: align final escalation fixtures * fix: preserve in-memory predecessor abort handoff * fix: restore controls for recovered queued messages * test: cover recovered queue controls * fix: close final steering review gaps |
||
|
|
60ca751a7f
|
🧠 fix: Preserve Deferred Tool Schemas Across HITL Resume (#14552)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🧠 fix: Preserve deferred tool schemas across HITL resume * 🧪 test: Harden deferred tool resume regression * 📦 chore: bump @librechat/agents to v3.3.10 |
||
|
|
78ec1940a2
|
🧹 fix: Clean Up MCP OAuth State Mappings on Uninstall + Reject Superseded Callbacks (#14549)
* 🧹 fix: Clean Up MCP OAuth State Mappings on Uninstall + Reject Superseded Callbacks Disconnecting an OAuth MCP server deleted its mcp_oauth flows but left the mcp_oauth_state:{state} mappings behind for the full TTL. Because flow ids are deterministic (userId:serverName) and the CSRF token is HMAC(flowId), a stale browser tab's callback could resolve its orphaned state to the NEXT flow for the same server, pass CSRF, burn the fresh flow's one-shot CSRF cookie, and fail the PKCE exchange, sabotaging the legitimate retry. - Add MCPOAuthHandler.deleteFlowAndStateMapping: reads the flow's stored state and deletes the mapping before the flow (mapping-first so a crash between deletes fails closed instead of recreating the orphan) - Route mcp_oauth deletions in clearStoredMCPOAuthState through the helper for both tenant-scoped and legacy flow ids - Reject callbacks whose state does not match the resolved flow's stored state: the only control distinguishing a superseded attempt from the current one on a deterministic flow id Fixes #14534 * fix: gate failFlow on state match in the OAuth error branch (Codex P1) The provider-error branch failed the resolved flow on CSRF/session alone, so a superseded error callback resolved through an orphaned mapping could mark the current flow FAILED. Apply the same stored-state equality gate before failFlow. * fix: leave the flow in place when the state-mapping delete fails (Codex P2) deleteFlow swallows storage errors and returns false, and deleteStateMapping discarded that result, so a failed mapping delete followed by a successful flow delete would silently recreate the orphan. Surface the boolean from deleteStateMapping and throw from deleteFlowAndStateMapping before touching the flow, so the caller's allSettled warn branch fires and the next replacement retries both. * fix: restore the state mapping when the flow delete fails (Codex P2) The inverse partial failure of the round-3 fix: a successful mapping delete followed by a silently failed flow delete left a PENDING flow whose reused authorization URL could never resolve, dead-ending every callback in invalid_state until the flow went stale. Check deleteFlow's result, re-store the mapping on failure, and throw so the caller's allSettled warn branch fires. * fix: never leave a callback-capable flow behind on uninstall (Codex round 6) Teardown runs after the server's tokens are deleted, so a preserved flow+mapping pair (the round-3 early-throw path) let a lingering consent tab complete the callback and recreate credentials post-uninstall. Now that both callback branches gate on stored-state equality, an orphaned mapping is the benign failure mode, so invert the order: delete the flow first, attempt the mapping delete regardless, and reject when either reports a storage failure. This supersedes the round-4 mapping restore, which also preserved a callback-capable pair. * fix: delete the flow even when its metadata read fails (Codex round 7) A storage error on the initial getFlowState aborted teardown before any delete ran, preserving the callback-capable flow after token deletion. Tolerate the read failure, delete the flow blindly, skip the mapping it could not identify (the callback gates neutralize the possible orphan), and reject so the caller's warn branch fires. |
||
|
|
f5e8feba80
|
📦 chore: bump @librechat/agents to v3.3.9 (#14548)
|
||
|
|
1e1de6eff9
|
🎯 fix: Exact Ask-Question Attribution via Interrupt tool_call_id (#14539)
The ask_user_question pause/answer stamps (server pause-time args stamp, resume-time answer stamp, and the client mirror) targeted the newest unanswered ask part by position. When a model emits several ask calls in one turn, the interrupt's question and the user's answer land on the wrong card. @librechat/agents > 3.3.8 surfaces the interrupting call's tool_call_id on the ask interrupt payload. All three stamps now target that id exactly when present, keeping the positional fallback for older payloads. The tool body passes config.toolCall.id through to askUserQuestion via a typed alias that is a no-op on the pinned SDK and lights up on the next dependency bump. Companion to danny-avila/agents#366, which also fixes the underlying dangling tool_use 400 (INVALID_TOOL_RESULTS) when one of the parallel ask calls streams malformed args. |
||
|
|
f0d3bcb622
|
📄 fix: Filter Non-PDF Documents on the Anthropic Encode Path (#14535)
Anthropic's Messages API only accepts application/pdf for base64 document sources, but encodeAndFormatDocuments sent every allowlisted file (docx, xlsx, csv, html) through the base64 branch unfiltered. The provider 400 recurs on every retry because attachments are re-encoded each request, permanently breaking the conversation. - Add isAnthropicDocumentType / isAnthropicTextDocumentType to data-provider, mirroring isBedrockDocumentType - Filter unsupported types before encoding (matching Bedrock semantics) and log the skipped attachments - Send textual types as plain-text document sources (source.type 'text'), which Anthropic accepts and supports citations for, instead of invalid base64 blocks Fixes #14485 |
||
|
|
8e165eb451
|
🔒 fix: Remove Owner Email from Agent owner_contact Fallback (#14541)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🔒 fix: Remove Owner Email from Agent `owner_contact` Fallback The owner-contact fallback for agents without an explicit support_contact exposed the owner's private account email to any VIEW-level caller via GET /agents/:id and GET /agents. The fallback now resolves a display name only (name/username/authorName): the User query no longer projects email, the resolver never returns one, and the shared AgentOwnerContact type drops the field. Emails are only served when the owner opts in via support_contact. * 🔒 fix: Reject Email-Shaped Owner Display Names in Contact Fallback OpenID and SAML strategies fall back to the account email for the user's name and username when no display-name claims exist, so the name-only owner fallback could still surface the email through those fields. The resolver now rejects email-shaped display-name candidates entirely. * 🔒 fix: Treat Any @-Containing Display Name as Email-Derived RFC-5321 quoted local parts may contain whitespace and the User schema email validator is an unanchored substring match, so such addresses can reach the name/username fields via SSO fallbacks. Rejecting on '@' presence covers every legal email form without re-fetching the account email. |
||
|
|
ad74a282d1
|
🪃 fix: Resolve User Vars Before the First Post-OAuth Reconnect (#14538)
* fix: resolve customUserVars before first post-OAuth-callback MCP reconnect
The OAuth callback route reconnects the user's MCP connection immediately
after storing new tokens, but never resolves customUserVars before doing
so - unlike the /reinitialize route a few hundred lines below, which does.
As a result, headers/oauth_headers templates like `{{MY_KEY}}` are sent
to the MCP server literally, unsubstituted, on this first connection
attempt, even though the user's value is already saved. The upstream
server rejects it as an invalid credential.
Fixes #14537
* refactor: share getServerCustomUserVars reader from @librechat/api
The mcp_-prefixed key shape was built by getUserMCPAuthMap but re-derived
by hand at each read site (a private helper in services/MCP.js, and the
new callback-route extraction). Export a reader from the same module that
owns the writer and reuse it at both sites, so the key shape has a single
source of truth.
* chore: sort destructured require members in routes/mcp.js
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
|
||
|
|
d6c2dc5d8e
|
🧵 feat: Background-Native Code Execution Tools (#14532)
The code-execution pair (execute_code/bash_tool) now defaults INTO background dispatch whenever the run_in_background capability is enabled, the same way the SDK's coding tools carry `intent` natively: enabling the capability is enough, with no per-tool or per-spec flag required. An explicit run_in_background: false opts the pair out (by definition name, marker projection, or a narrowing spec selection's wildcard), and the builder Code toggle flips to opt-out semantics: absent reads as on, and turning it off persists an explicit false. A spec's runInBackground: false now synthesizes an explicit wildcard opt-out instead of staying a silent no-op. Pre-native, false and absent were behaviorally identical, so a config that wrote false must not silently flip to backgrounding code. The ephemeral toggle's false stays no-policy: it is a badge default, not a decision. |