mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-28 12:44:28 +00:00
5230 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
92a8058f02
|
🛟 fix: Isolate Invalid Skills During GitHub Sync (#14735)
* fix: treat unrecognized SKILL.md frontmatter keys as warnings An unknown key in one SKILL.md failed that skill outright, and because the GitHub sync runner marks a source failed on any validation error, a single stray key took down every other skill in the repository. Syncing github.com/cloudflare/skills failed entirely because 2 of its 13 skills carry a `references:` key. UNKNOWN_KEY is now a warning, so the skill is stored (unknown keys and all) and the issue is surfaced rather than fatal. `references` joins the allowed set with a shallow JSON-safety check instead of a strict kind match: real files use a string, a list of strings, a list of objects, and a map, and pinning one shape would reintroduce the same failure. Malformed frontmatter stays fatal: INVALID_TYPE, INVALID_SHAPE and the non-plain-object check are unchanged. * fix: skip individual skills instead of failing a whole sync source Any error inside the discovery or commit loop reached the outer catch and marked the entire source failed, so one unusable SKILL.md, one oversized blob, or one duplicate name cost every other skill in the repository. Each skill now runs inside its own boundary and a failure is recorded against that skill. Errors that mean nothing else in the run can succeed (lock loss, GitHub auth failures, rate limiting) still abort the source rather than being charged to whichever skill hit them first. Skills are marked seen before the attempt, so the reconcile pass cannot mirror-delete the previously synced copy of a skill a later run can repair, and duplicate names now drop the whole colliding group instead of letting tree order pick an arbitrary winner. Status gains `partial` (published some, skipped others) plus a capped sample of the skipped skills with the reason for each. A run that publishes nothing and skips something is still `failed`, carrying the first skip's error. The skipped entries name repository paths, so they follow the same visibility rule as owner/repo/paths; the bare count does not. Sync warnings are logged too: a background run has no user-facing surface, so the log is the only place a maintainer sees why an upstream SKILL.md looks off. * test: cover skill sync warnings reaching the log An unrecognized frontmatter key no longer fails the skill, so a background sync has nowhere to report it except the log. Every mock in this spec returned an empty warning list, which left that path unexercised. * fix: describe nested frontmatter values in the shared skill type `SkillFrontmatterValue` allowed only scalars and string arrays, while the server has always stored `hooks` and `metadata` as JSON-safe objects, and now `references` too. A skill carrying any of them could not be represented by `TSkill`, `TCreateSkill` or `TUpdateSkillPayload` without a cast. The type stays free of `any` and `unknown`: values remain JSON-safe by construction, and the server keeps bounding depth, string length and array size when it validates them. * fix: protect moved mirrors and rolled-back counts when a skill is skipped Continuing past a failed skill exposed two problems that aborting the whole source used to hide. A moved skill's mirror keeps its old upstream id until the update lands, and only the new path was marked as seen, so the reconcile pass read the mirror as stale and deleted the very copy the skip path exists to preserve. The old id is now marked as seen too. Deletion counters were incremented when a stale name-conflicting mirror was removed, but never undone when the following commit failed and the mirror was restored. The run no longer stops there, so the status persisted a deletion that did not happen and the reconcile pass counted the restored row again. Counters are now rolled back when the restore succeeds. * fix: bound unknown frontmatter values and keep moved mirrors through duplicates Tolerating an unrecognized key meant its value skipped the shared JSON-safety check, so a deeply nested or oversized payload was accepted and persisted under a key nobody validates. The key stays non-blocking; the value is now held to the same depth, array and string bounds as every structured key. A skill that moves into a name another discovered skill also claims is dropped with the rest of its duplicate group before the sync path can reuse its mirror, which left the still-published copy unmarked and reconciled away. Both paths now mark the moved mirror through one helper. * fix: end the source when a skipped skill fails to roll back A skill that fails and rolls back cleanly is just a skipped skill. One whose restore or delete also fails leaves a mirror with half-rewritten files or a half-created row, and the run now continues past it, so the source could report partial success while that mirror stayed inconsistent and its pre-marked upstream id kept reconciliation away from it. Failed rollbacks now raise a source-fatal error carrying the original failure, which stops the source the way a lost lock or a refused GitHub token does. * test: cover a skipped skill discovered at the repository root A repository-level SKILL.md is discovered with an empty path, so this pins that a skip recorded against it still persists with the rest of the partial status rather than taking the whole status row down with it. * docs: describe unknown skill frontmatter warnings * fix: preserve mirrors after partial skill sync * fix: preserve skill validation details during sync * fix: fail sync when mirror identity cannot be restored * fix: harden skill sync failure boundaries * fix: preserve skipped skills on fatal sync * fix: surface skill sync diagnostics and rollback failures * fix: preserve skill frontmatter extension keys * fix: reject skill frontmatter keys that collide when normalized Frontmatter keys are matched case-insensitively against the canonical key list, so "Name" and "name" both resolve to "name". Every call site normalized independently, and the last key in iteration order silently won, meaning the effective value depended on YAML ordering rather than on anything the author could see. Centralize the normalization in normalizeSkillFrontmatterKeys and have it fail when two recognized keys resolve to the same canonical key, rather than picking one. parse.ts, deployment.ts and the agent handler now surface that as a parse error; createSkill and updateSkill surface it as a blocking DUPLICATE_KEY validation issue. Unrecognized keys are still passed through untouched so extension frontmatter survives. deriveStructuredFrontmatterFields and both write paths now run on the normalized map, so a "Disable-Model-Invocation" key derives the same column a lowercase one does. * fix: harden github skill sync against dropped requests and failed cleanup Three failure paths in the GitHub sync could leave a source looking healthier than it was. githubJson only handled HTTP-level errors. A fetch that rejected before producing a response (DNS failure, socket reset, abort) escaped as a raw TypeError, so the sync reported a generic crash instead of a typed sync error. Wrap it as GITHUB_REQUEST_FAILED and add that code to the fatal set, since a source whose requests never complete cannot be partially synced. When a synced file failed to persist, the orphaned upload was cleaned up on a best-effort basis and the cleanup error was only logged. If the cleanup itself failed, the source still ended with the original error and left a real orphan behind. Promote that to a rollback failure so the source reports SYNC_ROLLBACK_FAILED with the triggering error. Skill warnings were logged inside commitRemoteSkill, before the file sync and viewer setup that can still roll the skill back. A skill that never survived publication therefore emitted warnings as though it had. Return the warnings from the commit and log them once the skill is fully published. * fix: report skipped github skills before credential errors serializeErrorMessage checked isCredentialError first, and that check matches on the error text. A skipped skill whose path happens to contain a credential-ish word, for example skills/credential-helper, was therefore redacted to "GitHub skill sync credentials are not available" for admins without credential-metadata access, hiding a parse failure behind a wrong diagnosis. Check the promoted skipped-skill case first, since it is identified by error code rather than by text and is the more specific match. The credential redaction still applies to everything else. |
||
|
|
ae24461146
|
🚣♀️ feat: Smooth Streaming Text Fade-In (#14757)
* 🚣♀️ feat: Smooth Streaming Text Fade-In Adds a native FlowToken-style fade-in for streamed message text, with no changes to the streaming data path: a per-block rehype plugin wraps newly arrived words in one-shot CSS fade spans, using document-order character offsets so already-visible text never re-animates, even when markdown re-parsing restructures the tree. Words still inside their animation window replay identical props so in-flight fades are never cut short. - New Content/animate.tsx: word splitting (Intl.Segmenter for CJK), offset-based new-text classification, rehype plugin factory, and an AnimatedText component for plain-text reasoning content - MarkdownBlocks: per-block plugin instance appended to the cached rehype plugins only while animating; block memoization untouched (bench asserts identical code-block render counts) - Animation gated on isSubmitting && isLatestMessage and dropped at stream end, so settled messages render without wrapper spans - Skips code, pre, math/KaTeX, artifacts, citations, and MCP UI subtrees - Smooth streaming toggle (default on) under Settings → Chat → Messages; animation disabled under prefers-reduced-motion - Extends the streaming bench with a fade variant (+9% render time in jsdom, 62/62 code-block renders) and adds unit tests for the plugin, classification, and AnimatedText Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrUMn8aEKtMY2QRZhpheJB * 🔧 chore: Fix Import Order in Markdown and Settings Registry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrUMn8aEKtMY2QRZhpheJB * ✅ test: Cover Kana-Only CJK Word Segmentation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrUMn8aEKtMY2QRZhpheJB * 🛡️ fix: Harden Streaming Fade for Resume, Concurrency, and Reduced Motion Addresses Codex review on the smooth streaming fade: - Hydrated/resumed content becomes the animation baseline: a renderer whose first run already exceeds FADE_HYDRATION_THRESHOLD (reconnected stream, conversation switch) no longer re-fades the entire accumulated response - Classification is now transactional under React 18 concurrency: runs are staged during render and published via commit() from a layout effect, so abandoned renders (interruption, StrictMode double-render) leave no trace - prefers-reduced-motion now disables the rehype transform and AnimatedText in the render gate, not just the CSS animation, so reduced-motion users skip the span-wrapping work entirely Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrUMn8aEKtMY2QRZhpheJB * 🌏 fix: Refine Fade Hydration Signal, Spaceless Scripts, Collapsed Reasoning Second Codex review round on the smooth streaming fade: - Hydration is now an explicit signal instead of a per-block length guess: Markdown captures whether substantial content already exists at the render where its animate gate flips on, and passes it to each block's plugin. New blocks mounting later in the stream always animate regardless of size, and resumed content becomes the baseline regardless of block sizes - Word segmentation now covers all spaceless scripts (Thai, Lao, Tibetan, Myanmar, Khmer) in addition to CJK/Hangul, so continuously streamed text in those scripts keeps fading instead of freezing after the first window - Reasoning text no longer runs the word transform while the thinking panel is collapsed; expanding mid-stream starts from a hydrated baseline Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrUMn8aEKtMY2QRZhpheJB * 🖱️ feat: Hide Streaming Cursors While Smooth Fade Is Active The word fade itself signals streaming, so the trailing block cursor (result-streaming) and the pulsing thinking dot (result-thinking) are now suppressed whenever the smooth streaming fade is enabled. Both return when the setting is off or the device prefers reduced motion. Extracts the shared gate into a useSmoothStreaming hook consumed by Markdown, Reasoning, TextPart, DisplayMessage, EmptyText, and the legacy loading fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrUMn8aEKtMY2QRZhpheJB * ✅ test: Pin Cursor Assertions to Fade-Off State The pulsing thinking cursor now only renders while the smooth streaming fade is off, so the tests asserting it set the toggle off first (at file level where earlier renders would cache the atom's first read). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrUMn8aEKtMY2QRZhpheJB * 💫 feat: Restore Start-of-Run Dot With Fade-Matched Treatment The trailing streaming cursor stays hidden while the fade is active (the fade itself signals text arriving), but the pre-first-token dot returns: nothing else tells the user the run started before any text exists. Restyled to match the word fade rather than the classic size throb — it fades in on the same 250ms ease-out curve, then breathes on opacity, so "run starting" and "text arriving" read as one system. Applied only when the fade is active; with the setting off or reduced motion preferred the dot keeps its original pulseSize behavior, so the cursor assertions in the existing suites hold unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DrUMn8aEKtMY2QRZhpheJB --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ee8c0abe2d
|
🪝 feat: Execute Agent Plugin Command Hooks (#14755)
* 🪝 feat: Execute Agent Plugin Command Hooks Implement the missing PluginHookExecutor boundary so deployment plugins' ai.librechat/hooks/hooks.json documents execute instead of loading inert: - Command executor runs handlers as child processes outside the API process: Claude-shaped JSON payload on stdin, exit 0 + JSON stdout as sanitized hook output, exit 2 blocks with stderr as the reason, minimal allowlisted environment plus PLUGIN_ROOT/PLUGIN_DATA, abort-signal kill - Plugin loading carries the parsed hooks document on the contribution and threads hookCapabilities from startup, gated on the operator opt-in DEPLOYMENT_PLUGIN_HOOKS (off by default: parsed-but-inert with warning) - Runs register every ready plugin hook onto the per-run HookRegistry after internal policy hooks, with once-per-conversation SessionStart dedup Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Harden Plugin Hook Execution Boundary Address CI and Codex/Copilot review findings on #14755: - Break the agents -> plugins import cycle: the run seam now reads a PluginHookSource wired at startup (mirrors the tool-approval registry) - Tighten plugin ask decisions to deny unless the run has HITL wiring, so an un-resumable interrupt can never strand OpenAI-compatible callers - Scope cross-run dedup keys by authenticated user and handler identity: caller-supplied conversation ids cannot collide across principals, and sibling SessionStart handlers all fire; once handlers persist across runs - Replace a literal NUL byte in source with an escape (file diffed binary) - Kill the whole detached process group on abort, not just the shell - Map exit 2 on events without a decision channel to preventContinuation - Reserve PLUGIN_ROOT/PLUGIN_DATA against allowlist overrides, quote PowerShell args, cap captured output by bytes with one-pass decoding, and serialize payloads inside the executor's error boundary - Fix import ordering flagged by the static checks Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Close Plugin Hook Policy and Namespace Gaps Address the second Codex review round on #14755: - Drop updatedInput from plugin command outputs: hooks in one dispatch all receive the original arguments, so a plugin rewrite would reach the tool without the approval policy re-evaluating it (host-only now) - Translate Claude tool aliases (Bash/Write/Edit/Read) to LibreChat runtime names in matchers, with reverse payload mapping, so Claude-authored guards fire instead of planning ready and never matching - Key once-only state by declaration position as well as handler contents, so sibling declarations with identical handlers stay independent - Thread sessionStartSource through createRun and mark the HITL resume rebuild as 'resume', so SessionStart matchers see the real lifecycle Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Translate Regex-Form Claude Tool Aliases Address the third Codex review round on #14755: alias translation now substitutes word-bounded tokens, covering regex matchers like ^Bash$ and ^(Write|Edit)$ that the exact-token pass left registered against Claude names and silently never firing. A regex whose alias sits inside a character class or escape is rejected as unmapped so it fails loudly at plan time instead of never running. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Scope Alias Translation and Reuse Load-Time Plans Address the fourth Codex review round on #14755: - Add the WebSearch -> web_search alias so Claude-authored web-search guards fire against the LibreChat built-in - Apply alias translation only to tool-name events; a StopFailure matcher like ^Bash failed$ stays untouched and keeps matching the error text - Reuse each plugin's load-time hook plan at run registration instead of re-planning up to 512 handlers on every chat turn Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Translate Aliased Tool Inputs and Harden Hook Domains - Present aliased tool inputs under Claude field names (file_path, old_string, new_string, including nested edits), so Write/Edit/Read guards see the fields they check instead of silently allowing - Derive the alias table from canonical tool-name definitions (BashExecutionToolDefinition, CREATE_FILE_TOOL_NAME, Tools.web_search) instead of a parallel hand-authored table - Reject matchers naming Claude built-ins with no runtime equivalent (Task, Glob, Grep, WebFetch, ...) as unmapped at plan time instead of registering guards that never fire - Replace per-event Sets and Stop special-cases with an exhaustive EVENT_TRAITS record over HookEvent, so new engine events demand explicit semantics at compile time - Move cross-run once-state behind a PluginHookOnceStore seam with a least-recently-marked memory default: active conversations refresh their keys each turn, so capacity eviction can no longer re-fire a conversation that is still in use; the seam admits a shared-cache store for multi-replica deployments - Gate portable-only command handlers at plan time on Windows via a new supportsHandler capability (commandWindows or shell powershell required) instead of spawning bash that cannot exist - Kill Windows hook process trees with taskkill /t on abort - Require declaration indices on execution requests, stamped from the plan instead of defaulted at execution time Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Keep Group SIGKILL Escalation Armed After Wrapper Exit An aborted hook whose descendant ignores SIGTERM could leak that descendant: the wrapper shell's exit fired close, which cancelled the scheduled group SIGKILL. The escalation timer is now never cancelled — it is unref'd and killTree already tolerates a vanished process group, so a redundant late sweep is harmless while a surviving descendant is reliably killed at the grace deadline. killGraceMs is configurable on CommandExecutorOptions, with a regression test driving a trap-protected descendant past the wrapper's exit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Scope Once Retention by Conversation and Reject Clear Source - Restructure the once store around conversation scopes: registration touches the scope every run, so rarely-matching once handlers keep their keys while the conversation is active; eviction removes whole idle conversations (capacity counts conversations, not keys) - Reject SessionStart matchers naming the clear lifecycle source at plan time — no LibreChat run-construction path emits clear, so the handler would plan ready and never fire; wildcard warning text now reflects the sources that actually occur - Make the SIGKILL-escalation regression test real: the surviving descendant redirects its stdio away from the captured pipes so the wrapper's close fires while it is still alive, exercising the window a close-time cancellation would leak Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Bound Alias Tokens by Tool-Name Characters and Host Shells - Translate Claude aliases (and reject unsupported built-ins) only when delimited by characters that cannot appear in a runtime tool name: action tool names preserve hyphens, so an alias embedded in a longer name like deploy-Bash-v2_action_example_com stays the literal tool name instead of being rewritten into a matcher that never fires - Reject PowerShell-only command handlers on POSIX hosts at plan time (and skip them at runtime): bash cannot run PowerShell syntax, so the guard would fail open; a handler with both variants still runs its portable command - Handle rejected asynchronous once-store calls: a failed touch logs instead of raising an unhandled rejection during run construction, and a failed markOnce lookup fails open per the store's documented over-fire direction Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Probe Group Liveness Before Cancelled or Delivered SIGKILL The never-cancelled escalation timer could signal a recycled process-group id when an aborted hook's whole tree exits early in the grace window. Escalation now probes the group with signal 0: close cancels the timer only when the group is verifiably empty, and the deadline re-probes before delivering the group SIGKILL, so surviving descendants are still reaped while a fully-dead group never receives a blind late signal. The residual probe-to-signal race is documented as irreducible without pidfd support. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Gate Windows Escalation on Root-Process Liveness Windows taskkill /t walks the tree from the root process, so once Node observes the root's exit an escalation pass can reap nothing and a late forced taskkill could only hit a recycled PID. The liveness gate is now platform-aware in one helper: POSIX probes the process group with signal 0, Windows checks the root's observed exit state, and both the close-time cancellation and the deadline delivery consult it — no platform retains a blind late signal. Orphaned SIGTERM-ignoring descendants on Windows are documented as the platform limitation they are without Job Objects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Scope Payload Namespace to Declarations and Reap Stray Workers - Reverse name/input translation now applies only to declarations whose matcher actually required Claude-alias translation: the plan records requiresToolNameTranslation per entry, so a native-authored matcher like ^create_file$ receives native tool names and fields instead of Claude-shaped payloads its guard never expected - Coordinate the two dedup layers via a shouldExecute gate on the executor: a declaration suppressed by spent once-state declines before claiming the per-input dedup slot, so an identical handler under an overlapping matcher can still claim it and fire its own independent once-key instead of being permanently shadowed - Reap process groups that outlive a successful hook: a backgrounded worker left running after normal wrapper exit gets the same term-then-escalate sequence an abort uses, since unsupported async handlers mean no lifecycle owns such processes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🧰 chore: Vendor Pocock Codebase-Design and Architecture Skills Adds mattpocock/skills engineering/codebase-design and engineering/improve-codebase-architecture (MIT, license included) under .claude/skills so future sessions share the deep-module vocabulary (module, interface, depth, seam, adapter, leverage, locality) and the architecture-review process. Force-added past the /.claude/ gitignore deliberately; relocate if project skills should live elsewhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 refactor: Extract Process-Tree Reaping Into a Reaper Module Tree lifecycle — five of the last seven review findings — lived as event-handler wiring inside runCommand with its invariants in comments. It now sits behind a two-method seam: createReaper(child, graceMs) exposes reap() and onClose(), hiding the term-grace-escalate state machine, the per-platform liveness gates, the recycled-id guards, and the clean-exit sweep. The executor shrinks to capture-and-parse, and the reaper is unit-tested directly with real process trees through its own interface instead of only via whole-executor integration runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Scope Translation Per Alternative and Sweep at Root Exit - Track which runtime tool names alias translation produced, so a mixed-namespace matcher like Bash|create_file presents Claude-shaped payloads only for bash_tool invocations while the natively-authored create_file alternative keeps native names and fields; a capability omitting the produced-names list keeps declaration-wide translation - Sweep the process tree at root exit as well as close: a backgrounded descendant holding the captured pipes delays close until it dies, so the exit-time sweep terminates it promptly instead of stalling the hook until its timeout aborts - Pass the primary agent's resolved model and identity into the plugin hook context, so SessionStart payloads carry model and agent_type instead of always omitting them Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Default Wildcard Declarations to the Document Namespace - Matcherless (or wildcard) tool-payload declarations now inherit the hook document's Claude namespace: with no alternatives to carry namespace evidence, the plan marks them for declaration-wide reverse translation, so a wildcard guard inspecting standard Claude names and fields sees Write/file_path instead of silently failing open on native payloads; PostToolBatch entries translate the same way - Recognize aliases delimited by regex metacharacters: dots leave the tool-name boundary class (runtime names never contain them — action ids underscore domain dots), so ^Bash.*$ translates to ^bash_tool.*$ instead of registering a guard that never fires - Expand Claude's ${CLAUDE_PLUGIN_ROOT} spelling in hook commands and export it in the child environment alongside PLUGIN_ROOT - Scope SessionStart once-keys by lifecycle source, so a startup firing no longer suppresses the conversation's resume rebuild Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Normalize Claude Structured Hook Output Stock Claude hooks return decisions under hookSpecificOutput (permissionDecision/permissionDecisionReason), surface context there, and use continue:false plus the legacy approve/block decisions — none of which the sanitizer's native field names recognized, so a guard that works in Claude silently allowed in LibreChat. Parsed JSON now passes through a dialect normalizer first: hookSpecificOutput fields map to decision/reason/additionalContext, continue:false becomes preventContinuation, approve becomes allow, and block becomes deny on events that block by denying. Native fields win when both dialects appear, and the ask-to-deny gate applies to the Claude dialect too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Validate Native Decisions and Slim Once Keys - Strip malformed native output fields before the dialect merge, so a placeholder like {"decision":null} can no longer suppress a valid Claude permissionDecision into a silent allow; only recognized decision tokens take precedence - Preserve the caller's working directory in hook payloads: cwd now reports the run's session context instead of the plugin installation path, which commands already receive as PLUGIN_ROOT and which the executor still uses as each process's working directory - Store a compact sha256 digest instead of the full serialized handler in once keys: declarations may carry 32 KB commands and 256 args, and the previous key embedded them in every retained conversation scope Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc * 🪝 fix: Validate Decisions Per Event Channel and Control Post-Tool Blocks - Accept native decision tokens only from the target event's own vocabulary: "continue" is valid on Stop but malformed on a tool event, where it previously survived validation, blocked the Claude dialect merge, and was then dropped by sanitization into a silent allow - Translate a structured "block" on events with no deny channel (PostToolUse, PostToolUseFailure, and the other prevent-trait events) into preventContinuation with the block reason as stopReason, instead of discarding it and returning a reason that controls nothing - Document why LibreChat runs supply no payload cwd: tool paths address a remote code-execution sandbox rather than the API host where hook commands run, so no host directory describes the run Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWXQZD2WzeAsvee4eRdWWc --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
db43121073
|
⌨️ fix: allow file upload shortcut when editing (#14764)
Co-authored-by: Bas Schleijpen <bas.schleijpen@surf.nl> |
||
|
|
b2128a7d18
|
📡 fix: Preserve Redis Abort Terminal Delivery (#14749)
* fix(stream): preserve terminal delivery after Redis fences * fix(stream): cover Redis abort acknowledgment window * chore: sort stream timing imports * fix(stream): follow durable replacement handoffs * fix(stream): bound replacement handoff retirement * test(stream): cover handoff deadline drain * test(stream): cover fenced steer retirement grace * fix(stream): scope fenced retirement lifecycle * fix(stream): retire fenced subscribers safely * test(stream): settle subscription fixtures |
||
|
|
88e08c91e8
|
🧷 fix: Preserve Elicitation Answers Across Aborts (#14745)
* fix: preserve elicitation answers across aborts * chore: sort stream type imports * fix: guard malformed resolved answers * fix: close abort answer race gaps * fix: retain exact answers across pauses * fix: retain answers across resumed pauses * test: satisfy HITL fixture types * fix: retain legacy answers through approvals * fix: preserve answers in reconnect snapshots * test: type legacy answer fixture * fix: bind legacy answers to paused content * test: guard optional resume content * fix: resume questions without streamed content * fix: Preserve legacy answers through abort filtering * test: Narrow reconstructed abort fixture * test: Type abort transform fixture explicitly * fix: Isolate answers with missing ask content * test: Type missing-content ask fixture |
||
|
|
236ee6c1ab
|
🧭 fix: Re-Anchor Parent Activity Phase Bounds (#14741)
* test: cover parent activity phase finalization * test(e2e): stabilize parent phase coverage * fix(agents): reanchor parent activity phase bounds * fix(agents): preserve delayed tools in activity phases * test(agents): keep phase slice bounds typed * fix(agents): preserve sparse activity phase bounds * test(e2e): read structured phase replies |
||
|
|
ba29a6c5d6
|
🔐 fix: Preserve Plugin MCP Provenance Through Registry Storage (#14744)
* 🔐 fix: Preserve Plugin MCP Provenance Through Registry Storage Agent Plugins MCP servers are tagged `source: 'plugin'` by the plugin loader so `processMCPEnv` returns them verbatim, keeping any `${VAR}` a plugin declared literal. The registry derived `source` from the storage tier alone, so every startup server routed through `addServer(..., 'CACHE')` was retagged `'yaml'` — before inspection and again before persistence. That dropped the marker for deployment plugin servers merged into the startup MCP config, so `processMCPEnv` treated plugin-authored strings as operator-authored templates and expanded them from `process.env`. A malicious plugin declaring `Authorization: Bearer ${OPENAI_API_KEY}` received the host's key at its own endpoint, at both boot-time inspection and every runtime connection. `resolveServerSource` now carries an existing plugin marker through instead of re-deriving it, and is applied at all four tag sites (`addServer`, `addServerStub`, `inspectServerUpdate`, and config-tier lazy init, which hardcoded `'config'` and would have re-opened the same hole). The marker is only honored for operator-loaded tiers: a DB entry is user-authored and stays `'user'`, so user input cannot claim plugin provenance to escape the sandboxed placeholder rules. * 🔒 fix: Address Codex review — config-override provenance & upgrade re-tag Two follow-ups from the Codex review of the provenance fix, plus a test cleanup. P2 — a Config-tier override that shadows a same-name plugin base inherited the base's `source: 'plugin'` through the merge in `getServerConfig` / `getAllServerConfigs`, so `processMCPEnv` stopped resolving the operator's own `${VAR}` placeholders and silently broke their server. New `overlaySource` helper keeps an operator override on its own trusted source when the base is plugin-sourced; all other bases still inherit as before. Fails safe (never a leak), but the regression is real. P1 — the init fingerprint hashes only the raw MCP config, which already carried `source: 'plugin'` before the provenance fix, so the hash is unchanged by it. On a Redis-backed rolling restart with no config change, followers short-circuit on the stale `INITIALIZED_CONFIG_HASH` and the old `source: 'yaml'` plugin entries survive with no expiry — the fix never takes effect. Fold a `REGISTRY_STORAGE_SCHEMA_VERSION` into the fingerprint so an upgrade forces exactly one cluster-wide re-init. Also drop unnecessary `as` casts in the provenance tests (declare the fixture as `ParsedServerConfig`, assert with `toMatchObject`) and add a regression test for the P2 override case. |
||
|
|
e108955c20
|
🧷 ci: Enforce Durable Agent Finalization for E2E tests (#14740)
* test: enforce agent generation finalization * test(e2e): correlate canonical persisted turns |
||
|
|
01e9d119bf
|
🛻 ci: Move the ESLint Config Sweep Into Its Own Job (#14742)
The full-sweep regression gate lints api+client+packages twice — once under the PR's config and once under the base ref's — inside the same job as ~20 later steps (data-provider/data-schemas/api builds, config migration tests, unused-i18n scan, and four depcheck passes), all sharing one 30-minute budget. Two type-aware sweeps of the whole tree cost more than everything else in that job combined. When they run long the job hits its timeout mid-sweep, so every step behind the gate never executes and Static checks reports no result at all — strictly worse than not running the gate. continue-on-error: true hides this, because the step never fails; it simply never finishes. Move the gate to its own job with its own budget so it cannot starve the other checks, and bound each sweep so an over-budget run degrades to a notice rather than a failure — an unfinished sweep is no evidence of a regression, and the gate is advisory about config scope. Behaviour on a sweep that completes is unchanged: coverage loss and new (file, rule, severity) diagnostics still fail. |
||
|
|
ea6f9e3f4f
|
🌐 chore: Restore English Localization Source Values (#14743) | ||
|
|
60fe67d27e
|
🌍 i18n: Update translation.json with latest translations (#14739)
* 🌍 i18n: Update translation.json with latest translations
* fix: restore stable English translation labels
|
||
|
|
7347cfc195
|
🍡 feat: Batched User Questions With A Single Bounded Answer Form (#14737)
* feat: support batched user questions * test: align batched question fixtures * fix: harden batched question lifecycle * test: submit batched HITL answers in e2e * fix: address batched question review findings * fix: preserve invoke return typing |
||
|
|
d89b11d34d
|
🎛️ feat: Adopt Composer Density Tokens (#14730) | ||
|
|
c93609cb82
|
📸 fix: Guard Screenshot Export Against Main-Thread Freezes (#14733)
* 📸 fix: Guard Screenshot Export Against Main-Thread Freezes * 🧪 test: Cover Conversation Export Flows End-to-End * 🧪 test: Stabilize Export Spec CSV and Toast Assertions |
||
|
|
f7d9f36922
|
🎨 feat: Refine Client Colors and Sharing Dialogs (#14734)
* Refine client colors and settings interactions * Align dark dialog theme tokens * Preserve custom hover themes and badge contrast * feat: redesign sharing dialogs * fix: preserve theme compatibility and role menus * fix: address review findings and static checks |
||
|
|
dfd4d9dd81
|
🧩 ci: Close Workflow Path-Filter Gaps (#14728)
* 🧩 ci: Close Workflow Path-Filter Gaps Six trigger-filter gaps found by reading all workflows against the live dependency graph (AI-1755, codegraph FINDINGS §6l): - backend-review/frontend-review: root package.json/package-lock.json now trigger unit tests — a lockfile-only dependency bump previously ran zero backend or frontend unit tests while every test job installs from it - agents-integration-tests: widen to the three package src trees it builds and imports (was only src/agents/**) - cache-integration-tests: same shape — verified live that packages/api/src/flow/manager.ts (imported by mcp/oauth) matched neither integration filter - docker-smoke: plain Dockerfile had no PR-time validation despite shipping via dev-images/tag-images; new node-image-smoke job builds it, gated by paths-filter to Dockerfile/.dockerignore changes - dev-images/dev-branch-images: add config/**, skill/**, .dockerignore — the single-stage image COPYs the full build context - static-checks: eslint.config.mjs now re-triggers the lint job (gap carried over from eslint-ci.yml in the #14716 consolidation) - delete generate_embeddings.yml: fired on docs/**, which no longer exists, and its docs-root-path pointed at the same missing directory * 🧩 ci: Address Codex Review Findings - Build caches: all 26 build-* keys across 8 workflows now lead with root package.json + package-lock.json so manifest-only bumps cannot restore stale dists (data-provider embeds the root version); unifies the split key families (playwright already hashed the lockfile) - static-checks: config changes now gate on the ESLint config loading and applying to representative files, plus a report-only full-tree sweep (70 pre-existing errors at dev HEAD block a hard gate for now) - docker-smoke: the workflow file itself triggers the plain-Dockerfile build so job edits are validated - dev-images/dev-branch-images: re-include skill/**/*.md after !**.md so shipped deployment-skill Markdown rebuilds images * 🧩 ci: Gate Config Lint Sweep on Regression vs Base Config Second-round codex finding: the report-only sweep swallowed config-wide breakage in scoped blocks the representative files don't exercise. The sweep now lints the same tree under the PR's config and the base ref's config and fails only when the PR's config produces more diagnostics for some (file, rule) pair — pre-existing debt never fails the gate, and fixes are never penalized. Base-config unavailability degrades to the load gate with a notice. Outcome surfaced in the failure summary. * 🧩 ci: Harden Config Lint Gate per External Review - Coverage direction: fail when the PR config stops linting files the base config covered (set difference on linted files) — a mis-scoped ignores previously only removed diagnostics and passed both gates - Severity-aware fingerprints: (file, rule, severity) so warn->error escalations gate on a clean tree for that rule; downgrades still free - Hard-fail when the base commit is missing so a future shallow-checkout change cannot silently disable the gate; annotate fetch-depth: 0 - Tab-separated fingerprint keys (space-in-path proof), --config on both sweeps, EXIT trap for the base config copy, comment on why it must live at the repo root (flat-config pattern base paths) - Narrow skill md re-include with !skill/README.md: top-level README is documentation-only; runtime skill Markdown still rebuilds images |
||
|
|
9bb599435f
|
📎 fix: Re-enable Send After File Upload (#14727)
* fix: enable composer send after file upload * style: sort composer imports |
||
|
|
09cbd54f48
|
🪆 fix: Rebase Activity Phase Bounds over Sparse Content (#14729)
The aggregator writes content parts at provider-source indexes, which can skip slots and leave holes in contentParts. Array.prototype.map preserves those holes and the Map constructor iterates them as undefined, so rebaseActivityPhaseBounds threw "Iterator value undefined is not an entry object" at the end of every run with sparse content — deterministic with parent phase summaries enabled, on both the completion and resume paths. Build the identity map with an index loop that skips nullish slots. Holes must stay out of the map: one undefined key would falsely match every hole in previousParts as a retained part and corrupt the rebased bound. |
||
|
|
a3cec67e08
|
🪆 feat: Add Parent Activity Phase Summaries (#14721)
* feat: add activity phase summaries * fix: preserve activity phase lifecycle semantics * fix: satisfy activity phase type checks * fix: simplify activity phase status mapping * style: format activity phase changes * fix: rebase activity phase bounds after shaping * fix: link activity phase trace ancestry * fix: reconcile activity phase bounds * style: format activity phase reconciliation test * style: align activity phase assertion * fix: retain reasoning across commentary * fix: preserve activity phase boundary state * fix: detect renderable phase children * test: type parallel phase assertion * chore: bump agents SDK for activity phases * fix: retain unphased lane reasoning * fix: preserve tool group expansion across phases * style: format phase expansion regression * fix: preserve phase interaction state efficiently * perf: skip sparse phase segment holes * perf: partition phase segments with offsets * fix: preserve phase boundaries and cursor state * test: align activity phase regressions with CI * test: keep phase context mock hoist-safe |
||
|
|
c3a429ddcd
|
🎨 feat: Add Versioned Theme Foundation (#14709)
* 🎨 feat: Add Versioned Theme Foundation * 🧩 fix: Keep Theme-Aware Chip Actions Consistent * 🎛️ fix: Preserve Default Theme Geometry * 🪪 fix: Keep Theme Identity in Sync * 🧭 docs: Define Theme Styling Policy * 🧹 chore: Sort Theme Imports * 🧵 fix: Preserve Theme Compatibility Contracts * 🛡️ fix: Harden Theme Compatibility Boundaries * 🧵 fix: Publish Theme Appearance Preset * 🐳 fix: Include Theme Preset in Docker Build * 🪢 fix: Preserve Legacy Theme Compatibility * 🧭 fix: Harden Theme Lifecycle Boundaries * 🧱 fix: Align Theme Appearance Defaults * 🧬 fix: Record Persisted Theme Provenance * 🧭 fix: Preserve Theme Transition State * 🧷 fix: Preserve Legacy Theme Contracts |
||
|
|
7fc62023eb
|
🧷 fix: Safely Recover Runtime MCP OAuth Rejections (#14684)
* fix runtime MCP OAuth recovery * style: sort LC-008 imports * fix: single-flight runtime OAuth handlers * fix: retain transport OAuth failures for recovery * fix(mcp): preserve OAuth recovery connections * test(mcp): type request-scoped config fixture * fix(mcp): harden shared OAuth recovery * fix(mcp): bound OAuth recovery escalation * style(mcp): sort OAuth integration imports * fix(mcp): harden OAuth recovery boundaries * fix(mcp): abort shared recovery waiters * fix(mcp): bound request OAuth recovery phases * fix(mcp): close OAuth recovery ownership gaps * fix(mcp): retry borrowers closed by OAuth recovery * fix(mcp): drain borrowers before OAuth reconnect * fix(mcp): preserve eviction across OAuth recovery * fix(mcp): unify OAuth recovery leases * fix(mcp): serialize cache reuse with recovery * fix(mcp): make recovery checkout atomic * test(mcp): use numeric config timestamp * fix(mcp): reacquire recovery checkouts * fix(mcp): retain shared recovery disposal * fix(mcp): restart checkout after recovery takeover * fix(mcp): close recovery lifecycle gaps * refactor(mcp): deepen OAuth recovery lifecycle * fix(mcp): harden OAuth lifecycle disposal * style(mcp): sort OAuth lifecycle imports * fix: lease MCP OAuth lifecycle edges * fix(mcp): isolate shared OAuth flows from aborts --------- Co-authored-by: Dennis Schenk <dennis@gridonic.ch> |
||
|
|
26bcbb713c
|
📁 fix: Consistent Export Filenames Across Formats (#14708)
* 📁 fix: Consistent Export Filenames Across Formats Conversation exports produced different filenames per format: txt/md/csv go through export-from-json, whose default formatter replaces only the first whitespace run (non-global regex), while png/json downloads keep spaces. Normalize the filename once before export so every format yields Word1_Word2_Word3.ext; the preset JSON export shared the same defect. * 🧹 chore: fix import order via sort-imports |
||
|
|
87a8b9aa12
|
📡 feat: Route Web Search and Scrape Egress through the SSRF-safe Agent (#14606)
* feat(web-search): route outbound search and scrape requests through the SSRF-safe agent Build the SSRF-safe agents at the web-search tool-assembly site and pass them into the search tool config so outbound search and scrape connections are validated at connect time against their resolved IP, on every hop including redirects, consistent with the other outbound clients. Add allowedAddresses to webSearchSchema, reusing allowedAddressesSchema, so self-hosters can permit a deliberately-private search or scrape endpoint (for example a private SearXNG instance). The field is resolved directly from the webSearch config at the createSearchTool call site, not through loadWebSearchAuth, because it is config and not an auth credential. webSearchSchema is flat (providers are chosen by enums, not by counting keys), so the field is inert with respect to provider selection. Document the field and its operator warning in librechat.example.yaml, and assert the wiring in handleTools.test.js: the SSRF-safe agents are threaded into the search tool config, allowedAddresses is passed through when set, and omitting it still threads the agents with no exemptions. TODO awaits @librechat/agents release with the httpAgent hook: this consumes optional httpAgent/httpsAgent fields on the search-tool config that are not yet in a published @librechat/agents. package.json is intentionally left at the current version; bump it to the release that ships the hook before this lands. Validated locally against a revendored @librechat/agents build, not a published release. * 🛡️ fix: Apply allowedAddresses to the Web Search SSRF Preflight The connect-time SSRF agent already honors webSearch.allowedAddresses, but loadWebSearchAuth ran the isSSRFUrl preflight without it, so an admin-permitted private search or scrape URL was stripped before the agent could ever use it. Thread allowedAddresses and the URL's effective port through isSSRFTarget and resolveHostnameSSRF so the exemption is consistent across both SSRF layers. * 🛡️ fix: Validate Web Search Destinations and Defer to Configured Proxies Handing agents to createSearchTool covered only the connect-time DNS lookup, which Node skips for IP-literal hosts, and a configured proxy connects on our behalf without running that check. A literal private target such as http://169.254.169.254 could therefore reach the network. Route every resolved web-search destination through the existing applySSRFSafeAgentIfDirect contract so a blocked literal target throws before any request is made, and withhold the agents when a proxy owns egress, since one agent pair is shared by every provider and a direct-connect agent on a proxied connection would break the request while asserting protection the proxy's network context cannot provide. * 🛡️ fix: Keep Web Search SSRF Agents Under a Proxy and Restore Pooling Withholding the agents whenever a proxy was configured removed protection from every direct and NO_PROXY destination in exchange for preventing a failure that cannot occur: for an https target Axios substitutes its own CONNECT tunnel, so the injected agent is never used for the proxy connection. Only a plaintext http target keeps our agent and repoints it at the proxy, and only a proxy whose hostname resolves private then trips the connect-time check. Always pass the agents and exempt the proxy endpoint instead, deriving host:port from the same PROXY, HTTP_PROXY, and HTTPS_PROXY resolution the rest of LibreChat uses so the proxy hop stays reachable while destinations remain guarded. Axios already applies NO_PROXY per request, so bypassed routes keep enforcement with no extra logic. Drop the load-time destination validation. It duplicated the isSSRFTarget preflight for user-provided URLs, rejected admin values that were previously legal, and threw from inside loadTools, where both loader wrappers swallow the error and drop every tool for the turn rather than degrading web search alone. Build the agents with keepAlive and cache them per exemption list. A bare http.Agent does not pool, so the previous code replaced the pooled global agents for every search, scrape, and rerank call and allocated a fresh pair per turn. * 🛡️ fix: Reject IP-Literal Private Targets on Web Search Connections Node resolves nothing for a literal host, so the connect-time lookup never saw one: a destination or a redirect target given as http://169.254.169.254 reached the network. Redirect hops pass through the same createConnection, so checking the literal there covers both cases and removes the need for a maxRedirects control that createSearchTool cannot accept. Gate it behind blockLiteralHosts so only web search opts in. A caller that reaches a proxy or a deliberate private service by literal address must exempt it first, and the merged consumers of createSSRFSafeAgents have no such exemption, so enabling this everywhere would break configurations that work today. * 🛡️ fix: Keep IPv6 Brackets on Derived Proxy Exemptions The exemption parser accepts an IPv6 entry only as [ipv6]:port, so stripping the brackets produced fd00::1:3128, which carries three colons and is dropped as malformed. An IPv6 proxy therefore stayed unexempted and the connect-time check rejected it, failing every web-search request routed through it. Use the URL hostname as parsed, which already carries the brackets. * 🛡️ fix: Exempt Proxies Configured Through ALL_PROXY Axios resolves a proxy through proxy-from-env, which falls back to all_proxy in either case after <protocol>_proxy, so ALL_PROXY on its own is enough to route a request through a proxy. Exemptions were derived from PROXY, HTTP_PROXY, and HTTPS_PROXY only, leaving such a proxy unexempted and rejected with ESSRF. Derive the exemptions from the full set of variables that can put a proxy in front of these requests instead. The installed proxy-from-env 2.1.0 reads no npm_config variables, so those are deliberately not included. * 🛡️ fix: Drop the Unearned PROXY Exemption and Harden the Web Search Guard Nothing on this path consumes PROXY: Axios resolves proxies through proxy-from-env, which reads only <protocol>_proxy and all_proxy, and web search never calls applyAxiosProxyConfig. Exempting it therefore granted a bypass rather than preserving a working route, and a user-settable search URL that redirects to that address reached it and returned the body. Remove PROXY and proxy, and skip a socks endpoint for the same reason, since Axios cannot proxy through one. Tolerate a non-array allowedAddresses instead of spreading it, which threw out of loadTools and dropped every tool for the turn. The YAML path is schema-validated but the admin override path merges without parsing, so the value is reachable. Separate cache keys with NUL rather than a newline, so an entry containing a newline cannot collide with two separate entries, and bound the cache. Give the agents the idle timeout the global agents carry, which keepAlive alone did not restore. Reject a unix socket, which carries no host to validate. Also treat fec0::/10 site-local as private, matching the fe80::/10 handling beside it. Exercise the real resolver in handleTools.test.js rather than mocking it, so the wiring test now fails if the agents it threads do not actually block a private target. * 🛡️ fix: Derive Proxy Exemptions Through Axios's Own Resolver Unioning every populated proxy variable exempted addresses that never carry a request. proxy-from-env picks a protocol-specific variable before all_proxy and lowercase before uppercase, so an ignored value became a trusted host:port that a redirect onto a direct route could reach. It also normalizes a scheme-less value such as proxy.internal:3128 to an http URL, where parsing the raw string yielded an empty hostname and no exemption at all, breaking the proxy hop. Resolve through getProxyForUrl, the entry point Axios itself calls, so precedence, scheme normalization, and NO_PROXY match exactly and cannot drift. NO_PROXY covering everything now yields no exemption, since nothing is proxied. Declared locally rather than adding a types package, alongside the existing declaration in the same directory. Also revert the fec0::/10 site-local change. domain.spec asserts that boundary deliberately to prove the fe80::/10 mask does not over-reach, and the shared address schema still classifies fec0 as public, so a runtime block there would leave operators unable to configure the exemption. It belongs with those two together, not in this PR. * 🛡️ fix: Resolve Proxy Exemptions Against the Real Destinations Resolving against placeholder probe hosts applied destination-specific NO_PROXY rules to a host nobody dials. With NO_PROXY matching the probe domain but not a real provider, no exemption was derived even though Axios still proxied the actual request, so the agent rejected the private proxy hop with ESSRF. Resolve per configured destination instead, passing the values loadWebSearchAuth already resolved. Only plaintext http destinations are considered, since for an https destination Axios substitutes its own CONNECT tunnel and never uses the injected agent for the proxy connection, which is also why provider defaults need no exemption: every one of them is https. * 🛡️ fix: Accept Embedded-IPv4 IPv6 Forms in the Address Exemption Schema The runtime guard blocks 6to4, NAT64, and Teredo addresses whose embedded IPv4 is private, but the schema's local copy recognized only ULA, link-local, and the dotted IPv4-mapped form, so an entry such as [64:ff9b::a00:1]:8080 was dropped as a public literal. An operator reaching a private endpoint that way could not configure the exemption at all. Mirror hasPrivateEmbeddedIPv4 in the schema helper, which the surrounding comment already asks to keep in sync. Public embedded addresses stay rejected, since an exemption there has no defensive purpose. |
||
|
|
7cf4c3f73f
|
🧪 test(e2e): add Bombadil property exploration (#14462)
* test(e2e): add Bombadil property exploration * fix(e2e): address Bombadil review feedback |
||
|
|
54d7f04d71
|
🪶 feat: Resolve Explicit Subagents Lazily (#14714)
* feat: resolve explicit subagents lazily * fix: satisfy lazy subagent type checks * test: persist lazy subagent mutation through model API * style: format lazy subagent persistence test * fix: log lazy subagent depth limit failures * fix: harden lazy subagent resolution * fix: Yield during lazy cancellation test * test: Synchronize lazy cancellation setup * style: Format lazy cancellation test |
||
|
|
6bff5ba148
|
🧬 fix: Normalize Legacy MCP Null Headers (#14720)
* fix: normalize legacy MCP null headers * Move TokenExchangeMethodEnum import to a new position |
||
|
|
8da51562f5
|
🧜 feat: Open Mermaid Diagrams as Artifacts with SVG/PNG Export (#14713)
* feat: open Mermaid diagrams in the artifact panel with SVG and PNG export
Mermaid diagrams previously rendered inline only, and the artifact panel
routed every artifact through Sandpack even when no bundler was needed.
- Route Mermaid artifacts to a direct renderer in ArtifactTabs, moving the
Sandpack path into a lazily loaded SandboxArtifactTabs so opening a
diagram no longer pulls in the bundler chrome or the startup config.
- Add an inline artifact card that opens the diagram in the panel instead
of rendering the same diagram twice.
- Add SVG and PNG export from both the inline diagram and the panel
header, with size-capped canvas scaling and background compositing.
- Lazy-load the artifact panel in Presentation and ShareArtifacts.
- Accessibility: label the panel as a dialog on mobile with a focus trap,
make the mobile resize handle keyboard operable, restore focus to the
opener on close, and honor prefers-reduced-motion.
- Fix the generated Sandpack wrapper to serialize diagram source instead
of interpolating it into a template literal.
- Cover the new paths with unit tests and a cross-browser Playwright spec.
* fix: keep Mermaid artifact identity and render state per diagram
Addresses three review findings on the Mermaid artifact panel.
Mermaid fences do not consume a code-block index, so every diagram in a
message received the same `mermaid-${blockIndex}` and therefore the same
Recoil artifact key: expanding one overwrote the other, and both cards
read as selected. Mermaid fences now carry their own index sequence,
seeded per markdown block the same way the code and artifact counters
are, so the id stays stable across streamed tokens.
The panel renderer is keyed by artifact id, so switching directly
between two diagrams can no longer carry the previous render, its
dimensions, or its export payload across the boundary while the new
source debounces. Editing an open diagram still does not remount.
The preview Refresh action drives the Sandpack client, which a Mermaid
preview never populates, so it only covered the panel with a spinner.
It is hidden for Mermaid, which offers its own retry on render failure.
Also drops com_ui_mermaid_export_preparing and com_ui_mermaid_source,
which no longer have call sites, fixing the unused-i18n-keys check.
* fix: bind Mermaid preview and export to the artifact on screen
Three further review findings, all on state outliving what it describes.
The editor reset in ArtifactTabs only lands after commit, so the render
that switched artifacts still passed the previous artifact's editor text
to the freshly keyed renderer, which mounted showing (and exporting) the
diagram just navigated away from. Editor text is now ignored until the
reset catches up. SandboxArtifactTabs carried the same pattern and gets
the same guard.
Switching to the code tab unmounts the preview, but the export payload
survived it, so the toolbar kept exporting a diagram that was no longer
on screen and no longer matched an edited source. The renderer now
withdraws its payload on unmount, and the export action is scoped to the
preview tab.
The diagram canvas mounts only once there is a diagram to show, so the
ResizeObserver ran against a null ref while the placeholder was up and
never saw the real element. Wide diagrams were fitted to the default
700px and clipped in narrower panels. Observation now re-runs when the
canvas appears.
* fix: scope Mermaid artifact ids to the content part
Each content part renders its own markdown tree, so the per-message
Mermaid counter restarts at zero in every part. Diagrams sitting either
side of a tool call therefore both resolved to
`mermaid-artifact-${messageId}-mermaid-0`: one registration overwrote
the other and both cards shared a selection state. The part index the
message context already carries now takes part in the scope.
* fix: keep the Mermaid export menu reachable in fullscreen
The artifact panel gained a fullscreen mode on dev, which re-roots the
panel into the fullscreen element and portals the copy and version
popovers there so they stay visible. The Mermaid export menu portals to
the body, so once these branches met it opened outside the fullscreen
element and rendered invisible. It now takes the same portal target.
* fix: heal Mermaid registrations and cap PNG canvases after rounding
Two findings from the latest review pass.
Closing the panel unmounts Artifacts, whose useArtifacts cleanup wipes
artifactsState while the inline cards stay on screen. The Mermaid card
never observed that, so reopening one card restored only itself and any
other expanded diagram vanished from the version navigator until it was
clicked again. It now subscribes to its own slice and re-registers when
the entry goes missing, matching the self-heal ToolArtifactCard already
documents. The write is a no-op when the entry matches, so it settles.
Rounding each PNG side independently could carry the product back over
the 16.7M pixel budget the scale was picked to satisfy: 3129x50000
resolved to 1025x16374, which is 16,783,350 pixels and enough for a
browser enforcing the area limit to reject toBlob outright. Rounding
down cannot exceed the budget, since the bounding scale is derived from
it.
|
||
|
|
152dcf4721
|
🔗 feat: Shared Conversation Badge and Stable Share Links (#14712)
* feat: improve shared conversation links * test: Cover Shared Link Lifecycle * test: Cover Shared File Snapshots * fix: address review findings on shared links Stop double-decoding the conversation search term. Express already decodes req.query, so the route's extra decodeURIComponent threw URIError on any term containing a bare percent sign and mangled percent-escape-looking text. The sidebar already sent the term raw, so this failed there too. Advance a share's stored target to its branch tail when an update omits one. Updating from the conversation list could not resolve the tail and reused the stored target verbatim, silently republishing the same snapshot instead of the turns added since. Require revalidation on shared files. Updates now keep the shareId, so the file URL no longer changes and a cached response could outlive a revoked share-files choice; an ETag over the pinned snapshot fields keeps unchanged files on 304. * fix: keep the shared badge across conversation cache replacements isShared is derived per list request and absent from single-conversation payloads, so rename, pin, and the SSE conversation updates dropped it when they swapped a server response into the sidebar cache, hiding the badge until an unrelated list refetch. Carry the cached value forward in updateConvoInAllQueries so every replacing caller is covered, while an explicit value still wins. * test: mock syncStaticTools in server boot specs initializeMCPs now calls syncStaticTools when no MCP servers are configured, but the server boot specs stub ~/server/services/Config without it. Post-listen initialization threw, hit process.exit(1), and took the jest worker down until it exceeded the retry limit. * fix: address codex findings on the shared DataTable and file ETag Keep the published DataTable export bound to the legacy component and ship the design-system table as VirtualizedDataTable, so external consumers of @librechat/client keep the props they compile against. Fold the snapshot's stored location into the shared-file ETag, so a re-published output that keeps its size and revision but moves its object no longer revalidates to a stale 304. Auto-fill the table when a first page is too short to overflow its container, since pagination is otherwise only reachable through the scroll handler. * fix: re-scope share grants before publishing and retry stalled auto-fill Move the shared-link ACL expiration write ahead of the content update. The shareId survives an update, so a failed ACL write after the write-through left the new messages and file snapshot readable at the same URL while the owner saw a 500. Retry a rejected auto-fill fetch up to three times: an unscrollable table has no scroll event to fall back on, and the sentinel alone would strand it on the first page. * fix: follow regenerated branches and pin forks to the payload they saw advanceTargetToBranchTail only walked descendants, so a target replaced by a regeneration (a sibling, not a child) left the update parked on the obsolete branch and published none of the turns that followed. A childless target now hops once to the newest sibling the conversation continued under. A shareId survives an update, so an owner republishing between a viewer's load and their Continue click would resolve targetMessageIndex against different messages. The fork request now carries the payload's updatedAt and is rejected with 409 when it no longer matches; the viewer gets the current version pulled in and can retry. * fix: keep table sorting and legacy backfills from breaking share flows Restore the union formatting a local lint-staged prettier collapsed in data-provider types, which broke the CI lint run. Header clicks now toggle direction instead of cycling through an unsorted state, which the controlled tables translated straight back into the default and made one direction unreachable. Re-arm the auto-fill guard on a sort change: a re-sorted first page arrives with the same row count, and the guard would otherwise suppress paging on a container that still cannot scroll. Lazy fileSnapshots backfills no longer touch updatedAt. That timestamp is the revision a viewer's fork is validated against, so a legacy share's first read would have made the Continue click that followed it fail with a 409. * fix: break pagination ties by id and reset share state per conversation Both list cursors marked a page boundary with values that repeat: conversations by (sort field, updatedAt) and shared links by the sort field alone. Imported chats share a title and a timestamp, so every row tied with the boundary was skipped. Both now carry the boundary row's _id and sort by it last, and the shared-links cursor is an opaque composite the route still validates before querying. The share dialog outlives a switch between conversations, so a link with files disabled left the next conversation's dialog showing the switch off and quietly published without files. The stored choice now falls back to the enabled default, and a stale link no longer sits in the copy field. * fix: keep titleless shared links in the paginated list A share has no title default, and BSON orders a missing title before every string, so encoding the boundary as an empty string skipped the remaining untitled links when sorting Name ascending and re-admitted all of them descending. The cursor now carries the boundary's null rather than flattening it, and because $lt and $gt are type-bracketed against a string, descending adds an explicit clause for the untitled tail that a string comparison can never reach. * style: sort share method imports * fix: fail closed on orphaned share targets and guard snapshot backfills getMessagesUpToTarget walked levels from the roots and returned everything it had accumulated when the target was never reached. An imported or partially deleted branch whose parent is missing therefore published the whole conversation instead of the selected branch; the walk now returns nothing unless it actually reaches the target. A lazy backfill wrote fileSnapshots unconditionally, so a viewer's first read of a legacy link could land after a republish and restore the snapshot it replaced, re-authorizing the stable URL of a file the owner had just removed. The write is now conditional on the link still having no snapshot, and the stored one wins any race. Regenerating a message above the shared tail leaves the whole stored branch childless, so the target walk now climbs to the closest ancestor the conversation continued under instead of stopping at the stored tail's own siblings. Changing the search or sort also returns the table's viewport to the top, since the query holds the previous rows while it refetches. * fix: page through titleless rows on both sides of the cursor The route validator still required a string primary, so the composite cursor the data layer issues for a titleless boundary came back as a 400 and the shared-links table stopped at that page. Conversations had the same type-bracketing gap the shared links just closed: a name-sorted page could not reach conversations with no title, since a comparison against a string never matches a missing field. The cursor now carries the null and the filter spells out the titleless clauses for both directions. * fix: keep the share badge read-only and refresh rows on cell changes ensureLinkPermissions re-granted the owner ACL entry on every call, so rendering the header's shared badge turned ordinary navigation into a permission write. It now checks for the grant first and only migrates a link that still lacks one. A fork carrying a positional target but no revision falls back to the whole share, since nothing proves which payload the index was counted against. The memoized table row compared row data and selection only, so a cell rendering external state (the archived list's pending Restore, for one) kept its stale rendering until the row object itself moved; rows now also compare a marker that moves with the column definitions. * fix: keep the shared badge honest when a delete fails or a link remains A failed delete left the conversation looking unshared: the optimistic snapshot covered only the shared-link queries, not the conversation caches the badge reads. The cleared conversations are now restored with the rest. A conversation can hold one link per target message, so clearing the badge on delete is a guess. The conversation list is invalidated once the mutation settles, letting the server decide from the links that are actually left. * fix: refetch every cached conversation page after deleting a link The invalidation was pinned to page zero, so a conversation cached further down the sidebar kept the badge the optimistic update had already cleared even when another targeted link survived. * fix: treat a failed page fetch as a failed auto-fill React Query resolves fetchNextPage with an error result instead of rejecting, so the rejection handler never ran: the guard stayed armed on the unchanged row count and an unscrollable table could never reach the next page. * refactor: move the share request helpers into the typed backend Cursor validation, page-size clamping and the shared-file cache validator were plain backend logic sitting in the legacy JS route. They now live in packages/api with their own tests, and the route keeps only the Express-side wiring: reading query params, mapping domain error codes to status codes, and writing the response. Also carries the requested file choice into the cache entry the create and update mutations synthesize, since the response never echoes it and the dialog reads a resolved entry with no choice as the enabled default. * fix: hold auto-fill while the replacement page is in flight A search or sort swap keeps the previous rows and hasNextPage on screen while the new first page loads, so the re-armed auto-fill asked for page two against a query that was still fetching its first. An infinite query runs one fetch at a time, so that request could cancel or interfere with the one already out. Both tables now pass their fetching state and the guard waits for it. * fix: stop advertising links a deployment no longer serves The sidebar badge rendered from the derived flag alone, so a deployment that turned shared links off still told owners a link was live while the public routes were unregistered. The share dialog called the link a snapshot, but the payload populates the referenced message documents on every request: an edit to an already-shared message is visible immediately, and Update only adds newly referenced ones. The copy now says that. Scroll pagination inspects a resolved error the way auto-fill already does, since React Query reports a failed page that way instead of rejecting. * a11y: gate the shared conversation label on the feature flag The icon stopped rendering when a deployment turns shared links off, but the row still announced the conversation as shared to screen readers. Both now read the same condition. * fix: accept long title cursors and stop badge work the feature disables The cursor cap was tight enough that a Name-sorted page ending on a long title produced a nextCursor the next request rejected, stranding the rest of the list. It now sits well clear of anything the server can issue. The conversation list skipped straight into the shared-link lookup even where ALLOW_SHARED_LINKS is off, paying a round trip on the sidebar's first page for a badge that is never rendered. A regeneration is newer than what it replaced, so only a newer sibling counts: an older one that still has follow-ups is the branch the target was regenerated away from, and resuming there published turns the target had excluded. * fix: hold scroll pagination while a replacement page loads Resetting the viewport to the top after a search or sort change fires a scroll event, and the retained previous rows still report another page, so the handler asked for page two of a query that was still loading page one. * fix: keep the legacy share migration ahead of the owner-grant shortcut A legacy row keeps its marker until every grant it needs exists, so an owner grant on its own is not proof the migration finished. Reading the marker first means a half-migrated public link still gets its public grant, while a fully migrated one keeps the read-only settled path the badge lookup depends on. |
||
|
|
5c939d129b
|
🔌 feat: Add Agent Plugins (Experimental) (#14704)
* 🔌 feat: Add Agent Plugins v1.0.0 Support Implements the Agent Plugins 1.0.0 specification so LibreChat can load portable plugin packages: a `plugin.json` manifest, `skills/` holding Agent Skills, `mcp.json` describing MCP servers, and reverse-domain extension directories. - Validate the closed `plugin.json` schema, selecting rules from `$schema` without retrieving it. Unknown top-level fields and a non-object `extensions` field are reported and ignored; every other violation rejects the plugin. - Enforce plugin-root containment through realpath, including for paths whose leaf does not exist, and apply the narrowest failure boundary per component. - Map `mcp.json` onto LibreChat MCP options across stdio, Streamable HTTP, and legacy HTTP+SSE, bypassing the config loader's `${VAR}` process-env expansion so plugin values never resolve against the server environment. - Expand only `${PLUGIN_ROOT}` and `${PLUGIN_DATA}`, once and non-recursively, in `args`, `env` values, and `cwd`; supply both variables to the subprocess after configured `env`, and reject entries that declare them. - Discover skills from the immediate children of `skills/` only, reusing the deployment skill loader so plugin skills are ordinary deployment skills with a distinct id namespace. - Read LibreChat's `ai.librechat` extension directory and hand `hooks/hooks.json` to the Claude hook compatibility layer. - Load operator-installed plugins from `DEPLOYMENT_PLUGINS_DIR` at startup, merging their skills into the deployment skill registry and their MCP servers into the app config. Plugins never displace a configured server or deployment skill. - Add `cwd` to the stdio MCP transport, which the specification requires and LibreChat did not previously support. Component failures stay isolated: a malformed `mcp.json`, an invalid skill, or a bad hooks document never prevents the rest of a plugin from loading. * 🔒 fix: Contain Agent Plugins config at the runtime boundary Review of #14704 surfaced that every real finding sat where the loader's output crosses into LibreChat's existing runtime, not in the specification logic. The loader deliberately left plugin placeholders literal, but downstream layers re-processed the same fields and undid it. - Mark plugin MCP configuration with `source: 'plugin'` and return it verbatim from `processMCPEnv`. Without this a remote plugin could declare `Authorization: Bearer ${OPENAI_API_KEY}` and receive host credentials at its own origin. The gate reads the configuration rather than a caller-supplied flag, so no future call site can reintroduce the leak by omitting it. - Skip `preProcessGraphTokens` for plugin configuration as well; it resolves placeholders into headers, url, and args on the same path. - Reject plugin server names that change under `normalizeServerName`. Tool keys embed the normalized name while request-time resolution uses the raw name, so an unstable name published tools that nothing could resolve. - Reject `__proto__`, `constructor`, and `prototype` as server names, and merge plugin servers with `Object.defineProperty` and an own-property conflict check, so a package cannot reach a prototype setter or collide with an inherited member. - Enforce manifest-name uniqueness before components are accepted; two packages sharing a name would share one `PLUGIN_DATA` directory. - Isolate a failed data-directory creation to the single plugin instead of rejecting the whole scan. - Prefix rejected-plugin diagnostics with the directory, which is the only identifier a package without a valid manifest has. - Type extension namespace contents as JSON rather than `unknown`, and correct the header field-value comment to name obs-text. Verified end to end from the built package: a plugin declaring an environment placeholder in a header reaches the transport with the placeholder intact while operator-authored configuration still resolves normally. * 🔇 fix: Report Agent Plugin hooks that will not run The loader reads `ai.librechat/hooks/hooks.json`, but nothing registers the resulting plan, and startup supplies no hook capabilities. A package declaring hooks was therefore accepted in silence, leaving an operator to believe the hooks ran. Detect the document when no capabilities are registered and report it as unsupported, so the limitation is visible in startup diagnostics rather than inferred from behavior that never happens. * 🧯 test: Restore MCP startup test mocks Carries the two mock additions from #14711 so this branch can prove itself green. `initializeMCPs` now calls `syncStaticTools`, which the server startup specs do not stub, so they fail on every branch that has not picked this up. Drops out of the rebase once #14711 lands. Co-authored-by: Danny Avila <danny@librechat.ai> |
||
|
|
82de3bc422
|
🚦 ci: Reduce GitHub Actions Runner Pressure (#14716)
* ci: reduce GitHub Actions runner pressure * ci: enforce static check path exclusions |
||
|
|
1bd4455c2d
|
🧭 fix: Make MCP Catalog Redis Cluster-Safe (#14717)
* fix: make MCP catalog Redis startup cluster-safe * fix: stabilize Redis readiness gate * fix: type Redis readiness export * style: apply canonical import order |
||
|
|
92d4705f79
|
🧭 refactor: make the side panels behave the same way (#14695)
* style: unify chat input tool badge styling Every tool badge repeated max-w-fit and its own hand-written checked-state colour triplet. Move max-w-fit into CheckboxButton's base classes, where tailwind-merge still lets a consumer override it, and collect the accent colours into a single map so the palette lives in one place. Artifacts repeated the amber triplet a second time on its dropdown button; that now reads from the same map. * feat: add feedback when resetting model parameters The button did nothing visible on click, so with parameters already at their defaults it looked broken. Spin the icon a full turn on press and announce the change politely, matching the Agent Builder panel which already announced but had no visual counterpart. The animation replays on consecutive clicks via a reflow, and is gated behind motion-reduce. * fix: keep the prompt editor open when inserting a special variable Opening the variables menu moved focus out of the textarea, whose blur handler exits edit mode, so the prompt snapped back to its rendered preview as if it had been saved. Guard the blur against focus landing inside a menu, since Ariakit focuses the menu itself on open, and hand the menu a finalFocus target so focus returns to the textarea on close. Without the latter the editor stayed open but unfocused, which quietly broke click-away-to-exit. * feat: create prompts from a dialog instead of a dedicated page Prompts now open a dialog from the sidebar, matching how skills and MCP servers are created, and /prompts/new is gone. The dialog reuses the existing form rather than duplicating it, with a flag to drop the page-level chrome that has no place in a modal. Three things the modal exposed: - Radix locks pointer events on the body, so the portaled category and special-variable menus rendered but could not be clicked. They now render inline when hosted in a dialog, as SetKeyDialog already does. - The floating labels notch out the page surface, which left a visible chip against the dialog background in dark mode. The surface is now passed in rather than hardcoded. - Creating gave no indication anything was happening; the button now shows a spinner and blocks repeat submits. Create buttons for both prompts and skills use the submit variant, since both perform a write. * style: match prompt action button sizes The share button sat at 36px next to a 40px Use Prompt button in the preview. Drop the size override so it takes the icon variant's default, and bring its row-mates in the editor header along so that row stays uniform. * feat: load prompts by scrolling instead of paging The query was already cursor-based; the nav hook was slicing it back into one page at a time behind Prev/Next buttons. Flatten the loaded pages and let the existing scroll hook fetch as the list nears its end. useNavScrolling only fetched from a scroll event, so a first page that did not overflow its container produced no event and the rest of the list was unreachable. It now tops up until the list actually scrolls, which is why zooming in used to 'fix' it. * feat: pin panel admin settings and scroll only the panel content Each side panel scrolled as a whole, so its filter row and toggles slid away with the list and the scrollbar spanned the full height. Give every panel a fixed header, a scrolling content region, and a footer that holds the admin settings. The skills panel gains the standard filter input in place of its title and toggle-to-search icon; it also rendered admin settings twice, once from the filter row and once from the accordion. Memories drops its client-side paging, which only sliced already-loaded data, in favour of scrolling the full list. * fix: repair the skills create menu and icon-only dropdowns The create menu was built on Dropdown, which is a select rather than an action menu, and Dropdown applies its className to the popover as well as the trigger. Sizing the trigger therefore shrank the menu itself to 36px and clipped both entries. Rebuild it on DropdownPopup, which is what the rest of the app uses for action menus. Dropdown's icon-only trigger also kept its horizontal padding and laid the icon out in a full-width flex row, leaving too little room so the icon flex-shrank to roughly half its width. That affected every icon-only consumer, including the prompts category filter. * fix: correct the gap above the MCP server URL field The fieldset grouping the connection sections carried display: contents, which removes its box and with it the margin that space-y puts on it. The first section inside sat flush against the description while every other gap kept its 16px. * refactor: unpin a favorite in one click The row's overflow menu held a single Unpin entry, so opening it was pure overhead. Show the unpin button directly instead. Its hover surface matched the row's own hover colour exactly, so hovering changed nothing; it now uses a surface that differs in both themes, with a border carrying the contrast in light mode where the surfaces are close. Adds the tests for unpinning, which had none. * fix: stop prompt skeletons stacking on top of the loaded list The groups were rendered outside the loading branch, so a refetch with data already cached drew three skeletons above the existing rows instead of leaving the list alone. The three states are now mutually exclusive. * feat: add PanelContent to standardize side panel loading states Each panel decided for itself whether to draw a spinner, a skeleton, or nothing, and some replaced the whole panel rather than just the list. PanelContent owns the scroll region and the loading/empty/content decision so a panel cannot invent a fourth pattern. It takes isLoading rather than isFetching on purpose: a refetch that already has rows on screen should leave them alone. * feat: give the side panels row-shaped loading skeletons Each panel now loads with a skeleton built from the row it stands in for, rather than a spinner or nothing: the memory card's key and token pill, the MCP server's icon over name and description, the bookmark's icon and count, the prompt card's block. Memories previously replaced the entire panel while loading, so the filter you had just typed into disappeared. The skeleton is now confined to the content region and the header stays put. Loading also moves out of the list components, which had each grown their own copy of it, and into the shared PanelContent. * feat: show a loading state in the bookmarks panel Bookmarks had no loading state at all: it rendered straight into its empty state while fetching, so it flashed 'no bookmarks' before the list appeared. Thread isLoading through and give it the same header, scrolling content and skeleton as the other panels. * style: tighten the favorite row and unpin button Even padding on the row, the unpin button sitting a little closer to the edge, and no border until it is hovered. * feat: scroll the bookmarks list instead of paging it Bookmarks were already fetched in full, so the pager was slicing data that was sitting in memory. Render the whole list and let it scroll, the same as the other side panels. It also removes a latent drag bug: rows were reordered by their index in the unsliced array while the list rendered a page slice, so dragging on any page past the first moved the wrong row. * feat: load skills by scrolling instead of capping the list The skills panel fetched a single page of 50 and never asked for more, so a 51st skill was unreachable. Switch it to the cursor-paginated infinite query that already existed alongside it and wire the shared scroll hook, matching prompts and the other side panels. The list and its rows only ever read summary fields, so they now take TSkillSummary and the response no longer needs casting through unknown. * fix: stop mocking real modules as virtual in specs Seven specs mocked @librechat/client and librechat-data-provider with `virtual: true`, which is for modules that do not exist on disk. These do, so the flag keyed each mock to a path derived from the spec's own directory rather than the module's resolved id. The component under test resolves the real id, so whether it got the mock depended on the module id cache of whichever worker picked the file up. UploadSkillDialog was the one that bit: when the mock missed, the real Radix dialog rendered and portaled its content to the body, so every assertion reading from the render container failed with the input "not rendered" while it sat in a portal a few nodes away. * test: give the lazy bookmark chunk room to load Waiting for BookmarkNav means waiting for babel to transform its whole module graph on first require, which does not fit in waitFor's default second when the transform cache is cold or the machine is busy. The failure looked like a missed re-render but was just an import in flight. * build: recycle jest workers before the OS kills them Coverage maps accumulate for the life of a worker, so a full client run pushes workers past a gigabyte and the OS kills one, failing whichever suite it was holding at the time. Capping idle worker memory also cut the wall clock, since the run no longer swaps. * fix: give the dialog prompt labels a real backdrop Floating labels notch out the surface behind them so the input's border does not run through the text. The dialog variant asked for `bg-background`, which no longer maps to anything and computes to transparent in both themes, leaving the border visible through the label. `bg-surface-primary` is what OGDialogContent actually paints. * fix: resolve side panel review findings Send the removed prompt create page to a tombstone route so a stale /prompts/new cannot render a blank form or fetch the id "new". Drive the list footer spinner from isFetchingNextPage alone; the old showLoading flag was set on scroll and only cleared by a later scroll, so it stuck on after the last page. Retry the scroll auto-fill through a ResizeObserver: the fill bailed whenever the panel had no layout yet and nothing asked again once it got one. A collapsed sidebar keeps its panel mounted and laid out, so gate fetching on the sidebar being expanded rather than draining the catalog behind an invisible panel. Gate the MCP admin footer on the admin role, matching the memories, prompts and skills panels; the bordered bar rendered empty for everyone else. Replay the reset icon spin by remounting the icon. Toggling the class list lost the animation to the re-render that setConversation causes. Announce panel loading from a live region carrying its own text. The skeleton rows and the spinner are both aria-hidden, so labelling the region left nothing for a screen reader to read out. Cover the scroll hook, the panel content primitive and the prompt create dialog with unit tests, and point the prompts e2e spec at the dialog rather than the deleted page. * chore: remove unused translation keys com_ui_pagination and com_ui_select_or_create_prompt lost their last callers when the prompt list moved to infinite scroll and the empty prompt preview was dropped. Only the English file is touched; the other locales are generated externally. * Fix nav pagination retry loop * Fix prompt field IDs and skills pagination * Fix prompt dropdown ARIA IDs * test: stub syncStaticTools in the server bootstrap specs initializeMCPs now calls syncStaticTools from services/Config when no MCP servers are configured. Both bootstrap specs mock that module wholesale, so the call threw, the post-listen handler ran process.exit(1), and the Jest worker died four times over before the suite was reported as failing to run. |
||
|
|
667d97d668
|
⛶ feat: add fullscreen artifact previews (#14585)
* feat: add fullscreen artifact previews * fix: harden artifact fullscreen behavior * fix: address artifact fullscreen review feedback * test: use standalone Recoil type import * fix: portal fullscreen artifact menus safely * fix: raise fullscreen artifact menu portal * fix: keep fullscreen artifact tooltips visible |
||
|
|
ccb43bf32b
|
test(mcp): mock static tool startup sync (#14711) | ||
|
|
ed9542ed75
|
🧬 fix: Rebind Request Context After Remote Agent Auth (#14685) | ||
|
|
1bccc2bc18
|
📡 fix: Refresh MCP Tools After List-Changed Notifications (#14686)
* fix(mcp): handle dynamic tool list changes Co-authored-by: Pascal Garber <pascal@artandcode.studio> * test(mcp): fix CI validation * fix(mcp): keep dynamic tool catalogs live * fix(mcp): harden dynamic catalog lifecycle * test(mcp): use typed startup connection * test(mcp): isolate dynamic e2e fixtures * fix(mcp): refresh tools after reconnect * fix(mcp): close dynamic catalog cache gaps * test(mcp): update OAuth connection mocks * fix(mcp): preserve app snapshot ownership * style(mcp): sort connection imports * fix(mcp): close review race conditions * fix(mcp): preserve cache ownership edges * fix(mcp): harden recovery lifecycle * fix(mcp): guard tool-less app refresh * fix(mcp): fence distributed cache races * fix(mcp): retire stale connection state * fix(mcp): keep tool snapshots authoritative * fix(mcp): fence stale app tool publications * style(mcp): sort repository test imports * test(mcp): mock empty startup publication * fix(mcp): preserve app publication generations * fix(mcp): harden publication recovery races * fix(mcp): address tool catalogs by runtime config * fix(mcp): load scoped catalogs for assistant writes * fix(mcp): harden catalog publication recovery * fix(mcp): serialize forced connection replacement * fix(mcp): serialize ordinary creation with replacements * fix(mcp): harden catalog fallback boundaries * fix(mcp): close lifecycle fencing gaps * fix(mcp): preserve catalog authority on failures * fix(mcp): compensate failed catalog mutations * fix(mcp): fence catalog refresh ordering * style(mcp): sort agent loader imports * fix(mcp): cancel stale connection creation * fix(mcp): fence catalog coordination * fix(mcp): close catalog race windows * fix(mcp): harden cross-pod catalog fencing * fix(mcp): close catalog lifecycle edges * style(mcp): sort assistant imports * fix(mcp): reject stale recovery authority * fix(mcp): restore static catalog on every startup * fix(mcp): order app catalog publications * style(mcp): sort catalog revision imports * fix(mcp): separate catalog allocation and commit fences --------- Co-authored-by: Pascal Garber <pascal@artandcode.studio> |
||
|
|
ef38f362ec
|
📦 chore: bump @librechat/agents to v3.4.2 and npm audit (#14702)
* 📦 chore: bump `@librechat/agents` to version 3.4.2 * 📦 chore: bump `mermaid` to version 11.16.1 and update related dependencies * 📦 chore: bump `js-yaml` to version 4.3.1 in package-lock and data-provider * 📦 chore: bump `nanoid` to version 3.3.18 in package.json and package-lock.json across multiple packages * 🔧 fix: Remove stray `api/tsconfig.json` breaking e2e `~` alias An empty `api/tsconfig.json` was accidentally committed with the agents bump. Playwright's require hook resolves path aliases from the nearest path-config, checking `tsconfig.json` before `jsconfig.json` in each folder, so the empty file shadowed `api/jsconfig.json` — the only place `"~/*": ["./*"]` is defined. Every e2e spec that calls `cleanupUser` then failed on `Cannot find module '~/cache/getLogStores'` from `api/models/index.js`. - delete the stray file and gitignore it so tooling can't re-commit it - register `module-alias` in `cleanupUser` so backend requires resolve regardless of which path-config Playwright happens to find * 📦 chore: bump `@librechat/agents` to version 3.4.3 in package.json and package-lock.json |
||
|
|
493ffced46
|
🪝 feat: Add Claude Hook Compatibility Layer (#14412)
* feat: add Claude hook compatibility layer * fix: satisfy API declaration build * fix: normalize hook matchers and conditions * fix: align Claude hook lifecycle semantics * fix: preserve Claude hook execution semantics * fix: gate unsupported Claude hook controls * fix: refine Claude payload and matcher translation * fix: scope Claude hook conditions to tool events * fix: preserve mixed Claude hook documents * fix: honor Claude hook execution controls * fix: close Claude hook lifecycle gaps * fix: isolate Claude hook declaration state * fix: preserve Claude matcher semantics * fix: handle PreemptBoundary in plugin hook payload switch `PreemptBoundary` joined `HOOK_EVENTS` in @librechat/agents 3.4.2. It has no Claude counterpart and is absent from the compatibility EVENT_MAP, so it can never reach a registered declaration, but the payload switch must stay exhaustive so a future event fails the build rather than returning undefined. |
||
|
|
39f5f9d846
|
⚡ perf: Agent List and Model Selector at Scale (#14601)
* perf: cut serial round trips from the agent list query path
The agent list was the slowest path on first page load. Three separate
problems compounded:
- `getListAgentsHandler` chained its reads: two ACL lookups, the avatar
refresh cache probe and the viewer skill scope all resolved serially
ahead of the list query, and `attachOwnerContacts` added two more hops
after it. The four independent reads now resolve together, and the
avatar refresh runs alongside the list query instead of before it -
refreshed paths reach the response through `urlCache`, not through
whatever the list query happened to read. Serial hops per request drop
from 7 to 4 on a warm cache.
- The avatar refresh loaded the user's whole accessible agent set (up to
MAX_AVATAR_REFRESH_AGENTS) to discover which entries were S3-backed.
Scoping the query to `avatar.source` means deployments on any other
file strategy match nothing instead of walking the full set.
- `fetchAllAgentPages` walked cursor pages at the server's default size
of 100, and callers consume the flattened result, so every extra page
was a serial round trip for no benefit. It now requests the server
maximum. Measured over a 2,860 agent account: 29 requests / 1.65s
before, 3 requests / 0.29s after.
Also parallelizes the conversation file reads in `initializeAgent`. The
convo file refs and the execute_code thread walk share no inputs, and the
two code-file lookups depend only on `threadFileIds`, so the chain of six
serial reads on every turn collapses to two. This one is time to first
token the user waits through.
* perf: virtualize the model selector agent list
Opening the agents submenu with a large agent set froze the tab and could
kill it outright. With ~10k accessible agents the submenu blocked for over
15 seconds and took the heap from 96MB to 911MB. Four per-row costs were
being multiplied by the full list, which rendered unwindowed:
- `useIsActiveItem` allocated a MutationObserver per row (10,016 of them
for one dropdown). Replaced with an Ariakit store subscription, which
needs no observer at all and returns a boolean so a row only re-renders
when its own active state flips.
- `useFavorites` ran per row, opening a jotai subscription, a query
subscription and a mutation each time. Hoisted to one call per endpoint.
- Each row rescanned `endpoint.models` to recover `isGlobal`, a field the
parent had already discarded from the array it was mapping. The parent
now passes it down from a lookup map.
- The list itself is now windowed above 100 rows. Ariakit's composite only
knows about mounted rows, so arrow-keying to the window edge previously
found no next item and let focus escape the nested menu, closing it;
`handleBoundaryNavigation` scrolls the next index in, waits for it to
mount, then moves the composite onto it. Navigation inside the window is
left to Ariakit.
Open drops from >15s to 96ms, mounted rows from 10,028 to ~18, DOM nodes
from 123,346 to ~1,000, and the heap no longer grows. Verified in browser:
arrow keys track 1:1 to index 238 and back, and click selection works.
* perf: serve the model selector from the shared VIEW agent query
The model selector asked for EDIT-scoped agents whenever the marketplace
is enabled, while `useAgentsMap` and `useMentions` asked for VIEW. Since
the cache key includes the params, that was two distinct entries, so first
page load ran the paginated walk twice and held two copies of the whole
agent list in memory. Measured against a 10k agent account: 22 list handler
invocations per page load, now 11.
Collapsing the two by asking for the same permission everywhere would have
changed what the selector shows - under the marketplace the EDIT scope is
what makes it "My Agents", with discovery handled by the marketplace entry.
So the list endpoint now marks each row with `isEditable`, resolved from an
ACL read folded into the existing parallel batch (no extra serial hop), and
the selector filters the shared VIEW response instead of refetching. A
VIEW-scoped list for a user with 2861 visible / 361 editable agents returns
exactly 360 rows flagged editable, matching what the EDIT query returned.
`AgentSelect` deliberately keeps its own EDIT query: it reads `skills` and
`skills_enabled`, which `sanitizeViewerSkillScope` strips from VIEW-scoped
responses. It also only mounts when the builder panel is open, so it is not
part of the first-load cost.
The field is set unconditionally rather than omitted when false so that a
client talking to an older server sees `undefined`, keeps every agent, and
degrades to showing too many rather than none.
* fix: address review findings on the agent list at scale
Three issues from review, all confirmed against the code before fixing.
Avatar refresh no longer runs alongside the list query. `updateAgent` writes
through `findOneAndUpdate` on a `timestamps: true` schema, so refreshing an
avatar advances `updatedAt` — the field `getListAgentsByAccess` sorts and
cursors on. A write landing after the first page's snapshot moved that agent
ahead of the returned cursor, dropping it from every later page and silently
truncating the caller's flattened list. This was a regression introduced when
the two were parallelized; serializing them costs nothing on the common path,
because a cache hit returns without issuing any query, so only the
once-per-30-minutes miss pays for the ordering. The new test asserts the write
lands before the list snapshot and fails against the parallel version.
The virtualized list no longer inserts a focusable grid into the combobox.
`List` spreads its props onto `Grid`, whose defaults are `role="grid"`,
`containerRole="row"` and `tabIndex={0}`; inside Ariakit's listbox that added a
tab stop ahead of any row and put grid/row semantics between the listbox and its
options. All three are now neutralized so focus and ARIA stay with the combobox
items.
The list also resets to the top when the filter changes. `Grid` keeps its scroll
offset across prop changes and clamps an out-of-range offset to
`totalRowsHeight - height`, the end of the shorter list. Scrolling deep and then
searching landed on the tail: measured at row 626 of 667 matches, with only
those rows mounted and reachable by keyboard. Keying the list on the search
value restores row 0.
* fix: declare option position and set size for the virtualized model list
Once the model list is windowed, only the mounted slice exists in the listbox,
so a screen reader infers position and total from ~19 elements instead of the
real set — announcing "3 of 19" partway through 10,014 agents.
Model rows now carry aria-posinset and aria-setsize. The marketplace entry and
any model specs share the same numbering, because they are options in the same
listbox: declaring the values on some options while leaving others to be
inferred from the DOM would make the set internally inconsistent. Both are
omitted entirely when the list is short enough to render unwindowed, where the
DOM holds every option and the implicit values are already correct.
Verified against a 10,014 agent account: the marketplace entry reports 1 of
10015, the first models 2 and 3, and after scrolling to row 4999 the leading
mounted model reports 5001 of 10015 with 19 options in the DOM.
* 🩹 fix: Address Follow-Ups on the Agent List at Scale
Corrects residual issues in the agent-list perf work, all inside its own scope.
- Forward `idOnTheSource` through `PermissionService.findAccessibleResources`
so `getUserPrincipals` skips the user-document read. The list handler resolves
three permission sets per request and each was paying its own `User.findById`;
the auth strategies already normalize the field to a value or null.
- Gate the editable-set lookup on its own predicate instead of borrowing
`canReturnSkillConfig`. The two answer unrelated questions and only coincide
today, so redefining the skill flag would have marked every agent editable.
- Log mapping failures in the list response instead of swallowing them.
- Apply the walk page size after the caller's params in `fetchAllAgentPages`.
A caller limit only changed page size, never what the flattened walk returned,
so `defaultAgentParams`' `limit: 10` would have turned one request into 301.
- Carry `isEditable` on the agent rows the create and update mutations write
into the list cache. Mutation responses omit the field, so those rows lost it.
- Document `isEditable` as list-only, ACL-derived, and fail-open on absence.
- Restore the truthiness guard on the thread walk in `initializeAgent`. Widening
it to `!= null` made an empty `parentMessageId` issue a full-conversation read
against an anchor that can never match.
- Await `getConvoFiles` directly rather than calling `.then()` on it, restoring
tolerance for synchronous test doubles.
- Correct the avatar-refresh comment: the projection was never full documents,
and the real reason to filter is that an unfiltered budget is self-reinforcing.
Tests: both new `initialize` tests and both new backend tests are
mutation-verified; the concurrency test fails under either serialization order.
* fix: preserve ACL isEditable when merging agent mutation responses
Mutation responses omit list-only isEditable. Inferring true from write
success promoted VIEW-only rows into the editable subset for MANAGE_AGENTS
callers who can PATCH agents their ACL marks non-editable.
* fix: sort imports in agent mutations test
ESLint import-order check failed on the isEditable cache-preservation test.
* 🧷 fix: Carry isEditable Onto Duplicated Agent List Rows
`useDuplicateAgentMutation` prepended the raw duplicate response to the cached
list, and mutation responses omit the list-only `isEditable` field. The row
survived the "My Agents" filter only by failing open on `undefined`, so it would
disappear the moment a consumer read the flag strictly.
Duplicating grants the caller ownership, so the new row is editable outright;
this is the create case rather than the merge case `mergeAgentListRow` handles.
Last cache write on this path that did not carry the field.
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
|
||
|
|
a5b10c78cf
|
🛡️ feat: Add Batched MCP Authority Proofs (#14688)
* feat(data-schemas): add MCP authority proof substrate * feat(api): add default-off MCP authority fences |
||
|
|
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. |
||
|
|
5ee910e44c
|
🐋 chore: add opt-in alternative local stack experiments (#14678)
* feat: add search stack PoC infrastructure (Track 1)
Docker Compose stack for PLAN.md's new chat-search architecture:
ferretdb 2.7.0 + its postgres-documentdb 17 backing store (wal_level=logical
for the later CDC spike), a new dedicated chat_search_db (PostgreSQL 17 +
pgvector, non-default credentials, three least-privilege roles per the
Security roles section), and clickhouse (26.3 LTS). vectordb/rag_api are
untouched, per decision 3.
All four image tags verified against their registries via curl (ghcr.io
manifest lookups, Docker Hub tags API) before pinning. Host ports checked
against every existing compose file in the repo to avoid collisions.
The role-provisioning init script and healthcheck script were both actually
run against live containers once Docker became available mid-task: full
Mongo-wire round trip against ferretdb, real INSERT/SELECT proving the
writer's default-privilege grants and the reader's deny-by-default posture
against a throwaway pgvector table, wal_level and pgvector/pg_trgm extension
checks. One bug only surfaced at runtime and is fixed: psql does not
interpolate :'var' inside dollar-quoted DO $$ ... $$ blocks, so role
creation/idempotency uses \gset + \if/\else/\endif instead.
* 🔐 chore: require operator-supplied FerretDB credentials
The chat_search_db and ClickHouse services already refused to start without
operator-supplied passwords; FerretDB's backing PostgreSQL still fell back to
a working ferretdb/ferretdb pair, so the stack booted with a known credential
even though it holds projected chat content. That is the same shape as the
myuser/mypassword default the search plan calls out on the existing vectordb
service.
All four FerretDB credential references now use ${VAR:?} - compose, the
healthcheck script, and the README examples - and .env.example ships
REPLACE_ME placeholders instead of literals.
The differential-test harness at packages/data-schemas/misc/ferretdb keeps its
fixture credentials; it holds only throwaway test data.
|
||
|
|
51ed1fab4b
|
🩹 fix: Keep Edit Action Fully Hidden While Streaming (#14687)
* 🩹 fix: Keep Edit Action Fully Hidden While Streaming #14677 stopped the row-hover reveal from un-hiding the edit button, but the pencil still shows as a dimmed ghost mid-generation. The shared Button primitive sets `disabled:opacity-50`, which compiles to `.disabled\:opacity-50:disabled` — specificity (0,2,0). The hidden state used a plain `opacity-0` at (0,1,0), so the disabled style won and painted the icon at half opacity. Verified in Chromium against a running instance: only two opacity rules match the button, and the computed value was 0.5. Switching the hidden state to `!opacity-0` (Tailwind emits `opacity: 0 !important`) drops it to 0 while the sibling actions still reveal at 1 on hover. The existing unit test could not catch this: jsdom applies no stylesheet, so asserting class names never exercised the cascade. It now asserts the important modifier specifically, with a comment explaining why a bare `opacity-0` is insufficient. * 🧪 test: Browser guard for the hidden edit action The Jest spec can only assert class names — jsdom applies no stylesheet, so it could not see `disabled:opacity-50` (0,2,0) outranking `opacity-0` (0,1,0) and repainting the hidden pencil at half opacity. That is exactly how the ghost survived #14677 with a green suite. Asserts computed opacity in a real browser mid-stream, and asserts the sibling Copy action is at opacity 1 in the same breath so a hover that silently failed to register cannot make the check pass for the wrong reason. Verified to fail on the pre-fix build with `Received: "0.5"`, and to pass 3/3 after. |
||
|
|
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> |
||
|
|
e3b8e30327
|
👋 feat: Day-Aware Landing Greeting Schedule (#14633)
* feat: day-aware landing greeting schedule Replace the branching time-of-day greeting in Landing with a declarative schedule keyed by weekday and hour. Each slot maps to a translation key, with an optional personalized variant interpolating the user's name, and days without a custom schedule fall back to the default one. The greeting resolves after mount to keep server-rendered markup stable, arms a single timer for the next slot boundary instead of polling, and recalculates on tab visibility and window focus so a sleeping machine or timezone change does not leave a stale greeting on screen. * feat: rotate landing greeting variants by day Each schedule slot now holds a pool of variants instead of one line, and the active variant is chosen from the local calendar day, so the greeting holds steady across a slot but differs from one day to the next. Day-specific lines join their day's pool rather than replacing it. Raise the landing large-text cutoff to 56 characters so a personalized greeting with a long display name, or a longer translation of one, keeps the intended size, and add a test pinning every variant under that budget. * feat: add a dawn greeting slot between late night and morning 04:00 to 07:00 sits between the two moods the schedule had: too late for "up late", too early for "good morning", and the visitor could be up early or not yet in bed. Give it its own slot that plays on the ambiguity, and move the early bird line into it, where the timing actually fits. |