mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-06 14:39:10 +00:00
1004 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
aa35cd42b1
|
📬 feat: Add Durable Agent Trigger Delivery (#14925)
* feat: wire trusted agent trigger dispatch * feat: add durable agent trigger delivery * fix: annotate trigger envelope byte limit * test: isolate trigger startup in server specs * fix: fence trigger delivery during account deletion * test: isolate trigger service in user controller specs * fix: close trigger deletion admission race * fix: harden account deletion fences * fix: close durable trigger review gaps * fix: require offline stale-fence recovery * fix: type trigger lane sequence ids * fix: fence admin user deletion triggers * fix: make trigger deletion recovery durable * fix: harden offline user deletion * fix: serialize trigger lane publication * style: sort trigger delivery imports * fix: recover orphaned trigger publications * fix: preserve trigger recovery ordering * fix: fence trigger publication during purge * fix: defer remote trigger deletion fences * fix: close durable delivery cleanup races * fix: drain CLI generation owners before deletion |
||
|
|
f8f118ef29
|
🛰️ feat: Execute Generic Agent Trigger Deliveries (#14921)
* feat: add generic agent trigger dispatch seam * refactor: harden trigger dispatch contract * fix: annotate envelope depth alias * style: sort trigger envelope imports * fix: reject unknown trigger dispatch modes * fix: reject unknown trigger envelope versions * refactor: validate complete trigger envelopes * feat: add agent trigger execution host * fix: enforce trigger delivery contracts * fix: harden trigger admission path * fix: finish trigger cancellation handling * fix: retry strict steer rollout gaps * fix: retry paused trigger steers * fix: parallelize trigger admission setup |
||
|
|
57ea1137f6
|
🛡️ feat: Let Admins Restrict Stateful Workspace Scopes (#14910)
* feat: let admins restrict stateful workspace scopes * fix: enforce stateful scope policy across agent paths * fix: close stateful scope policy activation gaps |
||
|
|
485abef3fa
|
🥚 refactor: Default Agents to Preferred Stateful Workspace Scope (#14908)
* feat: add user default for stateful agent workspaces * style: sort stateful workspace imports |
||
|
|
f829aca9fb
|
🧩 fix: Align Tenant and MCP Configuration Resolution (#14904)
* fix: Align Tenant and MCP Configuration Resolution * fix: Preserve Operator-Owned MCP Entries * fix: Preserve Configuration Source Ownership * style: Normalize Middleware Import Order * fix: Preserve Process Server Precedence * test: Align Tenant-Aware E2E Setup |
||
|
|
fdc9c77f6e
|
🗄️ feat: Archive All Chats From Data Controls (#14885)
* feat: archive all chats from data controls
Adds an "Archive all chats" row under Data controls > Your data, next to
Shared links, with a confirmation dialog. It calls a new
POST /api/convos/archive/all endpoint backed by archiveAllConvos, which
archives every conversation currently visible to the user in a single
updateMany and refreshes the stats of every chat project the archived
conversations belonged to.
Temporary and retention-expired conversations are skipped: they are
already hidden from the chat list, so archiving them would only surface
them in the archived view. The update runs with timestamps disabled so
each conversation keeps its own updatedAt and the archived list stays
sorted by real activity.
Archiving a conversation now also drops the new-chat message cache alias
for it. A chat's first turn writes the same message array under both the
conversation key and the new-chat key, so without this the messages of a
just-archived chat kept rendering on the new chat screen until a reload.
Deleting already handled this; archiving did not.
* fix: keep archive-all state consistent
* fix: drop stale detail caches after bulk archive
* fix: harden archive-all request handling
* fix: reconcile archive batch failures
* Fix project stats refresh races and archive route boundary
* Fix archive-all review findings
* Fix archive scan index and partial-batch stats refresh
Reconcile project stats for already-committed archive batches when a later
batch fails, and index the archive scan as { user, _id } so non-tenant
pagination can use _id order.
* Fix Recoil reset after a partial archive-all failure
Refetch the submitted conversation on error and start a new chat only
when that conversation is still active and already archived.
* Fix archive recovery from resetting a newly opened chat
Re-read the active Recoil conversation after the archive-state lookup
resolves, so a slow getConversationById cannot start a new chat if the
user already opened another conversation.
* Fix project-stat reconciliation after archive races
Keep retrying optimistic project-stat writes instead of returning a
stale document after three lost CAS attempts, and retry destination
project discovery after a transient distinct failure.
* Fix archive reset and project-count increment races
Leave already-archived chats open after archive-all, recount new
project conversations instead of incrementing, and skip a delayed
increment when a concurrent refresh already recorded that chat.
* Recover destination projects after discovery retries exhaust
Keep committed conversation IDs when post-archive distinct fails, then
rediscover those projects in finally so a moved conversation's
destination still gets reconciled after the error is rethrown.
* Fix archive-all recovery batching and remount pending state
Recover destination projects in 500-id chunks so the final lookup
cannot exceed Mongo's command size, and share archive-all pending
state through a mutation key so Settings remounts stay disabled.
* Stamp bulk-archived chats and refresh the pinned cache
Bulk archive wrote only isArchived, so the archived table dated every
swept chat by createdAt and the default archivedAt sort dropped the whole
run into the legacy null group. Stamp one timestamp for the sweep; the
filter only matches unarchived chats, so an existing stamp cannot move,
and timestamps: false still preserves each updatedAt.
The pinned section fetches on its own key with a five-minute stale time,
so an archived pin kept rendering in the sidebar until that expired.
Invalidate it alongside the other lists on both success and failure.
Also drop the async from the failing-batch updateMany mock: its
Promise<never> is not assignable to the Query return type, while a plain
synchronous throw types as never.
* Bound archive recovery state with the sweep marker
Recovery held every committed conversation id for the life of the
request so the finally block could re-run project discovery after an
in-loop distinct gave up. Slicing that array into 500-id queries capped
the BSON command size but not the heap, so a very large history could
exhaust a worker mid-archive.
The archivedAt stamp already identifies exactly what this call
committed, so recovery is now one distinct scoped to it. That filter is
a prefix of the existing user/isArchived/archivedAt index, and the two
discovery call sites collapse into one filter-taking helper.
* Reconcile archive stats when a write outcome is unknown
A batch that commits but whose result never returns, a stepdown or a
connection drop between commit and acknowledgement, left archivedCount
at zero, so the finally block skipped both marker recovery and the stats
refresh. The chats were archived, so no retry could find them again: the
sweep filter no longer matches them and their projects kept stale
counts.
Both now key off the write attempt rather than the returned count.
Nothing else needs to change, because the marker is stamped by the same
write whose result went missing.
* Retry dropped project refreshes and guard stale pointer writes
Two ways a project could keep stale stats after archive-all.
A refresh that rejected was logged and dropped for good. Its chats are
archived, so no retry of archive-all can find them again to recompute
against, and the likeliest rejection is the recoverable one:
refreshChatProjectStatsForUser gives up when the project changed under
every compare-and-set attempt. Failures are now collected and replayed
once the rest of the run has stopped competing with them.
A save already in flight could also undo the sweep. Its conversation
document still said visible, so its tail took the pointer branch and
wrote lastConversationId back to a chat the sweep had just archived,
leaving the project advertising activity on a chat the workspace hides.
The pointer write now confirms the chat is still visible first, and
recomputes the project when it is not.
* Verify project pointers after the write, not before
Checking visibility before the pointer write only moved the race earlier:
a sweep landing between the check and the update still archived the chat
and cleared the project, and the write then restored it as
lastConversationId.
The check now runs after the write and repairs instead of preventing. A
sweep that lands earlier is caught here; one that lands later refreshes
the project itself, and refreshChatProjectStatsForUser compare-and-sets,
so it cannot commit a count it read before this write. Same single
indexed read as the check it replaces.
|
||
|
|
c519f26904
|
📦 chore: bump @librechat/agents to v3.6.2 (#14905)
|
||
|
|
27ed491a2a
|
🏷️ fix: Persist the Ephemeral Agent's Display Label as Sender (#14899)
* 🏷️ feat: Add getEphemeralSender and Cover the Ephemeral-Id Format * ♻️ refactor: Consolidate the Ephemeral Sender Chains * 🏷️ fix: Decode the Ephemeral Sender for Persisted Messages * 🏷️ fix: Mirror the Persisted Sender Chain in useGetSender * ✅ test: Widen the Custom-Endpoint Fixture Type * ✅ test: Expect the Spec Label in the Composer Placeholder * 🏷️ fix: Resolve the Sender from Exact Labels, Not the Lossy Id |
||
|
|
7d850c308a
|
🧠 feat: Add Live Reasoning Labels (#14893)
* feat: add live reasoning labels * fix: Stabilize reasoning label checks * fix: Address reasoning label review findings * chore: Bump Agents SDK for reasoning labels * fix: Reset reused reasoning step evidence * fix: Reconcile cleared reasoning labels * fix: Fence reasoning label resets * fix: Reset reasoning ownership before gap labels * fix: Preserve THINK type through label reset * test: Expect run-global reasoning revision |
||
|
|
5d88b8453e
|
🪟 fix: Resolve Context Windows and Pricing for Newer Model Families (#14877)
* fix: resolve context windows for Qwen3.5+ and newer model families Qwen3.5 model ids matched the `qwen3` key and inherited its 40,960 token window instead of their native 262,144, so pruneMessages dropped whole conversations down to the system message once tool output grew large. Add the 3.5 through 3.8 generations, Meta Muse Spark, Muse Glimmer and Llama 4, plus newer DeepSeek, GLM, Kimi, Grok, Mistral, Nova, Cohere and MiniMax entries. Llama 4 Scout is capped at 1M rather than its 10M native ceiling, since no host serves near that and a 10M value would stop pruning from ever firing. findMatchingPattern now matches a vendor-prefixed id on its model segment first. No map key contains a slash, so a prefix could only ever contribute a spurious longer match: moonshotai/kimi-k2 matched moonshot (8 chars) over kimi-k2 (7) and reported half the real window. * fix: price newer models and make the pricing test helper faithful Models added to the context map billed either at defaultRate or at an older generation's rate. Llama 4 and Muse Spark fell to $6/1M, Kimi K3 undercharged 5x through the kimi key, and Grok 4.5/4.6 overcharged through grok-4. Add rates for 21 models. tx.ts needs no matcher change of its own, since it receives findMatchingPattern by injection from @librechat/api. The vendor-prefix fix therefore already corrects moonshotai/kimi-k2, which was matching moonshot at $2.00/$5.00 rather than kimi-k2 at $0.60/$2.50. The data-schemas test helper implemented a different algorithm from the function production injects, returning the first reverse-order match instead of the longest, so the pricing suite was not describing real billing behavior. Mirror the real implementation. Every existing test still passes, so nothing was relying on the lossy version. Rates for the Qwen 3.x plus/flash tiers, DeepSeek V3.1/V3.2, Kimi K2.6/K2.7 and Nova 2 Pro are left as they are, since published figures disagreed by more than the current fuzzy match is wrong. * test: assert parity between the context and pricing maps Neither map imports the other, so a model added to one and forgotten in the other is silently wrong at runtime: it bills at defaultRate, or it has no window to prune against. A comment in tx.spec.ts claimed such a test lived in packages/api, but none existed. Exact key parity is the wrong invariant. Both maps resolve by longest substring match, so gpt-4-32k legitimately prices through the 32k bucket without a row of its own. Assert instead that every key resolves in the other map, with exemption lists covering the genuine gaps and a further test that fails once an exemption goes stale, so the lists shrink rather than rot. Verified the assertions actually fail: injecting a context key with no price, a price with no context window, and a rate for an exempted model each fails the expected test and names the offending model. * fix: correct GLM 5.3, Kimi K3 aliases, Grok tiering and prefixed overrides Four gaps found in review. glm-5.3 matched the glm-5 key and reported 204,800 tokens rather than the 1,048,576 it shares with 5.2. Add both the context and pricing rows. The dot-prefixed Kimi K3 ids were added to the context map without pricing counterparts, so moonshot.kimi-k3 resolved to moonshot.kimi and moonshotai.kimi-k3 to moonshot, billing completion at 2.5 and 5.0 rather than 15.0. Grok 4.5/4.6 carry a 500K window, which makes xAI's doubled rate above a 200K prompt reachable, but neither key existed in premiumTokenValues so long prompts stayed on the base rate. Add the premium entries. Matching the model segment of a vendor-prefixed id was too eager. EndpointTokenConfig is an arbitrary record and one built from OpenRouter is keyed by org/model, so an alias like org/model-latest could resolve to a bare model entry and pick the wrong configured context and rates. Keep a matched key that carries the vendor, and retry on the segment only when the whole id resolved to a prefix-only match. * refactor: type the model matcher as keys-only and drop the caller casts findMatchingPattern reads keys and never inspects a value, but its parameter was typed as a union of the two maps it happens to be called with, so every injection site cast into it. pricing.spec.ts already carried that cast and parity.spec.ts copied it. Name the intent instead. Both adapters now pass their map through unchanged, and a future change that starts reading values will fail to compile rather than slip past a cast. |
||
|
|
fb8ae881cf
|
⏱️ feat: Show Run-Step Durations On Tool Cards (#14892)
* ⏱️ feat: Show Run-Step Durations On Tool Cards Surfaces how long each tool call took, derived from the `closed_at` / `created_at` pair already carried by `on_run_step_closed` — the same event #14871 and #14873 use for the terminal status. No new event, no new SDK surface. The duration is stamped onto the content part at the same three sites as `runStepStatus`, so it survives a reload and a resumable reconnect rather than living only on the live React message: - `callbacks.js`, on the aggregated part before the event is forwarded - `RedisJobStore`, in the host-authored replay reconstruction branch - `useStepHandler`, on the live message Rendering lands in the shared `ProgressText`, which nine tool cards already use, rather than in each card: one place decides whether a duration is shown and how it reads, and the cards only forward the number. That keeps this from adding a tenth independent state derivation to a component family whose label/announcement/progress split is already the subject of AI-1810. The value is deliberately absent rather than zero whenever it would be a guess — no `created_at`, non-finite input, or a negative elapsed time from two clocks that disagree, which is now reachable because a step can be opened in one process and closed in another after a checkpoint resume. Sub-second durations are suppressed as noise, and it renders only on a settled, non-error card, where the slot is not already carrying the cancelled icon or the error suffix. For assistive technology the compact form (`3.5s`) is hidden and paired with a spoken equivalent ("took 3.5 seconds"), both inside the button, so the accessible name carries the duration without an `aria-live` region re-announcing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🎨 style: Sort Imports In Touched Files The import-sort gate runs against the files a PR changes, so pre-existing drift in `ProgressText.tsx` and `RedisJobStore.ts` surfaced on this branch. Both were already unsorted on `dev`; this is the sorter's output, with no semantic change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🐛 fix: Accept Partial Timestamps In Run-Step Duration Helper `getReportableRunStepDurationMs` declared its parameter as `Pick<RunStepClosedEvent, 'created_at' | 'closed_at'>`, where `closed_at` is required. That contradicted the function's own purpose: every guard inside it exists precisely to handle stamps that may be missing. The Redis replay branch reconstructs closures from persisted JSON and holds nothing stronger than "might be a number", so it failed to typecheck against the narrower signature. Widened to an exported `RunStepTimestamps` shape with both stamps optional, rather than asserting at the call site — an assertion would move the decision about what is trustworthy somewhere it cannot be enforced, which is the thing the helper exists to centralize. Callers holding a fully-typed event still pass, since a required field satisfies an optional one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🐛 fix: Suppress Duration When Failure Arrives As errorSuffix Alone At every call site `error` carries cancellation while failure travels through `errorSuffix` with `error` false, so gating the duration on `!error` alone rendered "· 3.5s" beside "· failed" — and announced it. The gate now checks both terminal-failure channels. The original test pinned only the `error: true` path, which is why this survived; the failed-via-suffix path is now pinned separately, both the visible and the announced half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🧩 refactor: Persist Raw Run-Step Durations, Threshold At Render Only The three stamp sites filtered through the 1-second reportability threshold before persisting, baking a presentation rule into stored data: a 900ms step stored nothing, making "fast" indistinguishable from "not derivable" and unrecoverable if the display rule ever changes. Stamp sites now persist the raw `getRunStepDurationMs` value — absent only when genuinely not derivable — and the renderer alone decides what is worth showing, which `ProgressText` already did. Rendering is unchanged. `getReportableRunStepDurationMs` is removed; it existed only to serve the write-time filter, and a test now pins that sub-threshold durations survive to storage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🐛 fix: Suppress Duration On Backgrounded Bash And Code Cards A backgrounded call's run step closes when dispatch returns the handle, so the stamped duration is the dispatch time. Rendering it beside "Running/Finished in background" misstated a detached task's runtime as seconds — and violated the "settled card only" rule, since the card is still tracking the detached run. Scope is exactly the two cards that parse background handles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🌍 fix: Format The Sub-10s Decimal For The Active Locale The fractional seconds value was interpolated as a raw JS number, which hardcodes the en-US decimal point into every language — "1.4s" where the locale writes "1,4 s" — and translators cannot fix a number formatted in code. The value is now formatted via Intl.NumberFormat with i18n.language, following MessageTimestamp's pattern of threading the language into the util; plural-key selection stays on the numeric value. A malformed language tag falls back to the plain number. Also documents the two accepted limits of the derivation, so they read as decisions rather than oversights: positive clock skew is undetectable from a single stamp pair, and the value is wall-clock elapsed, so a step held open across a suspension (checkpoint resume, HITL approval wait) includes that time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🐛 fix: Persist A Durable `backgrounded` Marker Through Harvest; Localize Minute Digits Codex round 3, both findings confirmed. **Background origin survived only as transient state.** The dispatch handle in `tool_call.output` and the live status-marker attachment are both gone once the harvester patches the settled task's stdout over the handle — so the round-2 suppression (`backgroundHandle == null`) came back on after harvest or reload, showing dispatch time as the task's runtime. Following the same rule as e4bd15d (persist facts, decide at render): the harvest patch now stamps `backgrounded: true` onto the tool call in the same atomic write that erases the handle — on the heal path too, which re-applies over full-row saves that reverted the part. The cards gate on handle-or-marker; the dispatch duration itself stays stored. **Minute-branch digits bypassed locale formatting.** The seconds branch went through Intl.NumberFormat while minutes interpolated raw numbers, so Arabic/Persian locales flipped to ASCII digits above one minute. All interpolated values now flow through the (renamed) formatDurationValue; an ar-EG test pins the localized digits. data-schemas cannot be installed in this environment (same npm ci 403 as packages/api), so message.ts/harvest.ts are syntax-checked with resolution off and otherwise verified by review; CI runs their real typecheck and suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🧪 test: Assert The `markBackgrounded` Stamp In Harvest Expectations The successful-harvest test's exact `toHaveBeenCalledWith` object did not include the newly forwarded `markBackgrounded`, so the API suite would fail on it. All three harvest-call expectations now assert `markBackgrounded: true` — the exact-object one of necessity, the two `objectContaining` ones deliberately, since the durable stamp (on the best-effort file-failure path and the reapply heal alike) is now part of the behavior under test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🎨 style: Wrap Harvest Spec Expectation Per Prettier Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
bce93f9c55
|
🎯 refactor: Infer Agents Endpoint for Model Specs Naming an Agent (#14889)
* 🎯 fix: Infer Agents Endpoint for Model Specs Naming an Agent A model spec whose preset names an `agent_id` but omits `endpoint` was unusable. `isModelSpecEndpointMatch` compares the request's endpoint to `preset.endpoint` by strict equality, so an undefined endpoint matched nothing and every request selecting the spec was rejected with a bare `Model spec mismatch` — an error naming neither the spec nor the missing field. The selector had the matching half of the same gap: `handleSelectSpec` read `preset.endpoint` directly, so it sent no endpoint and skipped assigning `agent_id` to `model`. Fixing only the server would leave the request malformed, so the resolution is shared between both. - Add `resolveModelSpecEndpoint` to `librechat-data-provider`, inferring the agents endpoint when a preset names an agent and none is set. An explicit `endpoint` always wins, so configured specs are unaffected. - Use it for endpoint matching and in the selector, so the menu and the request pipeline resolve a spec identically. * 🔁 refactor: Materialize Inferred Spec Endpoints at Config Load The review showed the lazy-resolver approach was unsound end to end: config validation rejected an endpoint-less spec before the resolver could ever run (`tPresetSchema` requires the `endpoint` key), and the resolver was applied at 2 of ~8 read sites, leaving selection handlers, startup presets, access filters, and provider-key reachability reading the raw preset. Materialize once at the boundary instead: - `tModelSpecPresetSchema` now makes `endpoint` optional (`nullish`). This is barely a widening — `endpoint: null` already validated — and only for model-spec presets; `tPresetSchema` is untouched. - `materializeModelSpecEndpoints` writes each spec's resolved endpoint back onto its preset. `createAppConfigService` applies it at both effective-config assembly points — YAML base load and DB-override merge — so admin-panel specs stored in override documents are covered. Identity-preserving, so cached configs see no new references when nothing needs filling in. - Every consumer now reads complete specs; the client's lazy resolve in `handleSelectSpec` is reverted to a raw read. `getModelSpecPreset` and the two hand-rolled preset constructions resolve the endpoint explicitly, which the narrowed preset type now enforces at compile time for any `TPreset`-shaped destination. - `isModelSpecEndpointMatch` keeps the resolver as request-time defense. * 🩹 fix: Materialize Spec Endpoints Before the YAML Missing-Endpoint Guard `processModelSpecs` warns and skips any spec whose preset lacks an endpoint, and it runs inside `loadBaseConfig` — so the previous commit's materialization received a YAML list from which the inferable spec had already been dropped. Only DB-override specs (merged after the guard) actually benefited. - Materialize at the entry of `processModelSpecs`, so inference happens before the guard and YAML agent specs survive it. The guard keeps skipping genuinely endpoint-less specs. The `createAppConfigService` calls stay: the base-path one guards alternate `loadBaseConfig` implementations, the merged-path one covers override documents, and both are identity-preserving no-ops when specs are already complete. - Constrain the widened schema: omitting `endpoint` is only legal when the preset names an `agent_id`. A preset with neither validated as a hard error before the key became optional, and silently accepting it would trade that startup-time error for a dead spec. An explicit `endpoint: null` (valid before this PR) keeps validating. * 🩹 fix: Infer Only From Non-Empty Agent IDs, Never Over Explicit Null Two edge cases in the inference contract: - `agent_id: ''` (what a form-backed writer persists for an untouched field) passed the nullish checks, validating and materializing a spec that names no agent. Both the refinement and the resolver now require a non-empty id, so such config fails validation loudly instead of producing a selectable spec that cannot work. - `endpoint: null` alongside an `agent_id` was treated as inferable, silently activating a spec that validated — and was skipped — before this PR. An explicit null is a statement, not an omission: the resolver now infers only when the key is absent, preserving prior behavior for previously valid configs. |
||
|
|
06bf324cf0
|
🛤️ feat: Per-Agent Code Execution Routing With Stateful Session Scopes (#14848)
* feat: route code execution per agent profile * chore: sort execution profile imports * test: preserve stateful environment literal types * fix: isolate stateful code environments by user * fix: preserve per-agent code routing end to end * fix: route code priming by execution profile * fix: isolate code profile lifecycle state * fix: preserve mixed-profile code resources * fix: complete stateful skill routing |
||
|
|
d411512a98
|
⬆️ chore: Bump @librechat/agents to v3.6.0 (#14890)
* ⬆️ chore: Bump `@librechat/agents` to v3.6.0 Bumps the pin in `api` and `packages/api` from `^3.5.1` to `^3.6.0`. The caret on `^3.5.1` cannot cross the minor, so both manifests and the lockfile need the explicit bump. v3.6.0 contains three changes over v3.5.1, all additive: - `fix: Close Subagent Child-Graph Run Steps` — subagent child graphs run via `workflow.invoke()` outside `Run.processStream`, so the terminal sweep never reached their steps. They now close on both the success and error paths, which is what makes `on_run_step_closed` reliable for subagent tool cards. - `fix: Restore Run Steps Across Process Resumes` — open run-step lifecycle state is now persisted in LangGraph checkpoints, so a step opened by one process closes correctly after a resume on another. - `feat: route code execution per agent profile` — new optional `codeSessionKey` partition for code-session ids and file refs. No breaking changes: every new field on the public type surface is optional, and the package's own dependency set is unchanged between the two versions (verified against the registry), so the lockfile diff is limited to the `@librechat/agents` entry itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🔒 chore: Sync `bun.lock` with the agents v3.6.0 bump `bun.lock` still recorded both workspace requirements and the resolved package as `@librechat/agents@3.5.1`, which no longer satisfies `^3.6.0`, so `bun install --frozen-lockfile` would reject the committed state. `bun install --lockfile-only` cannot run in this environment: bun stores no integrity for the `xlsx` URL dependency and therefore re-fetches `cdn.sheetjs.com`, which the sandbox network policy denies (403 on CONNECT). The entry was updated directly instead, which is exact here because the package's dependency graph does not move between the two versions: its `dependencies`, `peerDependencies` and `optionalPeers` at 3.6.0 are identical to 3.5.1 (checked against the registry), so only the version, the resolution id and the integrity hash change. The integrity matches the one npm resolved into `package-lock.json`, and the two existing `@librechat/agents/*` hoisting overrides stay valid because the dependency set they resolve is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
eb3b353712
|
📡 fix: Publish App-Level MCP Tool Catalogs Without a Reserved Revision (#14858)
* 📡 fix: Publish App-Level MCP Tool Catalogs Without a Reserved Revision Shared MCP servers advertised no tools to agents, so every turn failed with "configured to use MCP tools, but none are available" (#14857). `replaceAppServerTools` returned false whenever a publication carried no `publicationRevision`, but only `refreshChangedTools` reserves one. Every other app-level publisher — the first-connect snapshot, reinitialization, on-demand catalog reads, the retained-catalog restore — was silently dropped. The agent path fails closed on that drop: the skipped write returns null, so reinitialize yields no tools and the turn 503s. Startup hid it. `connectAppServers()` defers the initial refresh and calls `refreshToolList()` itself, which does reserve, so a boot that reaches its MCP servers looks healthy. Only a lazily created app connection — the server not yet up when LibreChat boots, a dropped connection, a cold cache — takes the unreserved path. `ConnectionsRepository` now reserves before its own `tools/list`, matching the list_changed path; a failed reservation publishes unordered rather than failing the connection. Publishers with no pre-fetch reservation point have already fetched by the time they reach the cache, so they take the next revision at write time instead of being discarded. `mergeAppTools` still publishes at revision 0 and stays deferential to a live catalog. * 📡 fix: Bind App Catalog Ordering to the Fetch That Produced It Addresses review feedback on the previous commit: allocating a revision at publish time lets a slow `tools/list` of an old catalog outrank a newer one that reserved after it started, and it would let the retained-catalog restore — which republishes deliberately pre-mutation data — outrank a live catalog. Ordering now travels with the data. `fetchToolsSnapshot` reserves before its first page and returns the ticket on the snapshot, so every app-level publisher reads the revision belonging to the read it is publishing rather than one allocated at an unrelated moment. `fetchOrderedToolsSnapshot` carries the refresh's revision when it defers to one, since that is whose catalog it returns. With the reservation at the single point where app-level tools are read, no publisher can forget it, so `replaceAppServerTools` goes back to refusing an unordered write: a publication that lost its ticket fetched at an unknown time and cannot be ordered. A failed reservation is reported as `orderingUnavailable` rather than swallowed, which keeps the list_changed path retrying instead of publishing a catalog that would be silently dropped, and leaves inspection unaffected by a transient cache outage. `MCPServerInspector.getToolFunctions` becomes `getToolCatalog` and returns the revision with the tools, so there is no variant that quietly discards ordering. * 📡 fix: Retry an Empty App Catalog That Could Not Reserve Ordering Review follow-up. The no-tools-capability branch destructured the reservation result and dropped `orderingUnavailable`, publishing without a revision when the revision store was transiently unavailable. That write is rejected in silence, and unlike the snapshot branch this one returned without reaching `refreshToolList()`, so whatever the server last advertised stayed in place until the connection was recreated or the cache expired. Both branches now route an unreservable catalog through the same retry path. * 📡 fix: Serve Tools Whose Shared Catalog Write Could Not Be Ordered Review follow-up. Only the shared catalog write needs ordering; the tools themselves were just read from the server and are correct to serve. Discarding them because the write could not be ordered is what turns a cache failure into a server that appears to have no tools at all, which is the reported symptom. `updateMCPServerTools` now returns the tools it built when the publication has no reserved revision, instead of null. A superseded write still discards — there another replica holds something newer. Reinitialization also asks the connection to republish under backoff when its snapshot could not reserve ordering, so the shared catalog does not stay cold until something else triggers a refresh. * 📡 fix: Surface a Discarded App Catalog Instead of Debug-Logging It #14857 went a release without a diagnostic because the only trace of a dropped app-level catalog was a debug line no deployment runs. Operators saw agents fail every turn with nothing in the logs to explain it, and the reporter had to read the source to find the cause. A publication discarded because it cannot be addressed or ordered means this server's tools are unavailable to every agent that selected them, and serving an unpublished catalog means every request re-fetches it. Both are warnings now. A superseded write stays at debug: concurrent replicas produce it routinely and the winner already holds newer tools. Tests pin the level, so a later refactor cannot quietly make the failure silent again. * 🧪 test: Pin the Reinitialize Path's Catalog Ordering Reinitialization is the path an agent falls back to when the shared catalog is cold, so it is where #14857 surfaced as "configured to use MCP tools, but none are available". Nothing pinned that it forwards the ordering its snapshot was fetched with, nor that it asks the connection to republish a catalog it could not order. Both assertions fail against the pre-fix source. |
||
|
|
a2ad0aa0c8
|
🤐 feat: Allow Promptless Sends When Files Are Attached (#13717)
* ✨ feat: Allow sending file attachments without a text message When an agent asks the user to upload a document, the user could attach the file but still had to type a placeholder message ("OK", "Here is the file") before the send button enabled and the submit guard let the message through. Attachments now count as submittable content: - New isSubmittableMessage(text, fileCount) util: non-whitespace text OR at least one attached file. - ask() in useChatFunctions uses it instead of bailing on empty text, so an empty draft with attached files submits. - SendButton receives the attached file count and enables accordingly. - ChatForm only marks the text field as required when no files are attached, so react-hook-form validation no longer blocks handleSubmit. Submitting an empty draft with no attachments is still rejected at all three layers. Fixes #13646 * Address review: support replayed file-only turns + drop empty vision text - ask(): count replayed attachments (overrideFiles) in the submittable check and skip it entirely for regenerate, so a file-only message can be regenerated or saved-and-resubmitted instead of being rejected as empty. - formatVisionMessage(): omit the text content part when the message text is empty. Anthropic rejects empty text content blocks with HTTP 400, and an empty block adds nothing for other providers; image-only sends now format cleanly. Added formatMessages tests for with-text and image-only (Anthropic + other) cases. * Address review: keep attachment-only turns valid for providers, answer mode, and titles - formatMessage: substitute minimal text when a user turn carries files but no inline content, so Anthropic does not reject an empty user message for RAG or code-environment attachments. - assistants chatV1: send the same stand-in for attachment-only Threads messages, which reject an empty body. The persisted message keeps empty text. - ChatForm: attachments no longer make an empty draft submittable in answer mode, where submitText consumes the click without answering or sending. - agents request: seed title generation from attachment filenames when the turn has no text, so immediate-mode titles are not invented from an empty string. - useChatFunctions.regenerate.spec: mock the utils barrel over the real module so new exports resolve. * Cover the agents path for attachment-only turns AgentClient formats its payload with the SDK's formatMessage, not the local one, so the earlier guard missed the endpoint the feature actually targets: an attachment-only turn still reached Anthropic as an empty user message. Apply the same stand-in after the file-context and quote merges, so a turn that already gained inline content is untouched. * Carry filenames on freshly attached files The fresh-file submission mapping copied only file_id, filepath, type, and dimensions, so the attachment-only title fallback read an undefined filename and produced nothing. Include filename, and cover it with a test that submits an empty draft with one attachment. * Address review: cover assistants v2, fresh agent attachments, editor, and title fallback - agents client: the current turn has no files during buildMessages, so read the resolved attachments from message_file_map instead. The previous guard only ever fired for persisted historical turns. - assistants chatV2: the default assistants endpoint routes here, so it needs the same stand-in body chatV1 got. - assistants title: fall back to filenames, then the response, and keep the default title rather than saving an empty one. - EditMessage: retained attachments make an empty edit submittable, matching the composer, so the overrideFiles replay path is reachable from the UI. --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com> |
||
|
|
88747f0ad8
|
🩺 fix: Render Stopped Run Steps From Explicit Status (#14871)
* 🩺 fix: Render Stopped Run Steps From Explicit Status Tool calls decided "still running" vs "stopped" with a whole-message heuristic: const cancelled = !isSubmitting && progress < 1 && !hasError; That inference cannot tell which step actually stopped. An aborted step keeps spinning while `isSubmitting` is still true, and when submitting ends, every unfinished part flips to "Cancelled" at once regardless of which one died. `@librechat/agents` v3.4.6+ emits `on_run_step_closed`, a terminal per-step signal carrying `status` and timestamps — including for steps swept at end-of-run because the caller aborted. The pinned 3.5.1 already ships it; nothing consumed it. - `StepEvents.ON_RUN_STEP_CLOSED` plus `RunStepClosedEvent` / `RunStepStatus` types mirroring the SDK payload. - `PartMetadata.runStepStatus` — a dedicated field, since `status` is already claimed by activity-label and question-form parts. - Server handler forwards the event without the visibility gating the other step handlers apply: a step whose open reached the client must get its close, or the client is left inferring again. - `useStepHandler` writes the terminal status onto the tool call part. - Both decision points (`ToolCall`, the shared `useToolCallState`) prefer explicit status, keeping the heuristic as fallback for messages saved before this and endpoints that do not emit the event. Threaded through the five cards sharing `useToolCallState`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🩹 fix: Address Codex Review On Run Step Closure Rendering - Persist the terminal status server-side. The handler emitted the closure without folding it into `contentParts`, so the status existed only on the live React message: a reload or resumable reconnect dropped it and fell back to the very heuristic this fixes. Now stamped onto the aggregated tool-call part (via `stepMap`, falling back to the event's own index) before forwarding. - Honor terminal status independently of output parsing. Gating on `hasError` meant a `failed` step with unparseable output rendered as "cancelled", while a `failed`/`cancelled` step whose output did parse as an error was not terminal at all and shimmered indefinitely when no completion event arrived. A closed step now forces progress complete and reports `failed` as an error state on its own authority. - Pass the status to the second `BashCall` branch, which rendered the same updated component without it. - Reuse `Agents.RunStepClosedStatus` in `PartMetadata` instead of redeclaring the union, so a future SDK status cannot diverge between the event and the persisted part. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * ♿ fix: Replay Closed Status On Redis Resume, Announce Failures - Apply closure events during Redis reconstruction. The stamp added in the previous commit mutates only the originating process's in-memory `contentParts`; a resumable reconnect landing on another replica rebuilds from `RedisJobStore.getContentParts`, whose allowlist omits `on_run_step_closed`. The status was therefore absent from the sync snapshot and, being snapshot-covered, never redelivered as pending — so multi-replica resume fell back to the whole-message heuristic. Handled as a host-authored event alongside `on_steer_applied` and `on_activity_label`, since the SDK aggregator has no notion of it. - Announce terminal failures in the live region. Forcing terminal progress for a closed step meant a `failed` tool reached the `aria-live` region through `getFinishedText()`, which only special- cased cancellation and otherwise announced "completed function" — telling screen-reader users the opposite of what the card showed. A regression introduced by the previous commit; error states now announce failure before any completion string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 * 🎯 fix: Resolve Closed Steps By ID, Never By Index The steer and HITL offset wrappers clone and shift only `ON_RUN_STEP` and `ON_AGENT_UPDATE`; every other event passes through untouched. A stored `on_run_step_closed` therefore carries the SDK's unshifted index, while the part it belongs to was rebuilt at the shifted one. Any run containing a steer insertion or HITL resume would stamp the status onto an earlier tool card, or none — leaving the real card on the fallback heuristic while mislabeling a different one. - Redis reconstruction builds a step ID -> index map from the replayed `on_run_step` payloads (which carry the shifted index) and resolves closures against it, mirroring what the live callback does via `stepMap`. - The live handler drops its `?? data.index` fallback for the same reason. Skipping is the safe failure: a missing status degrades to the old heuristic, whereas a misplaced one actively mislabels the wrong card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vLhxCFMYkCaTsoFTiAjJ5 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
cd4511038d
|
🚏 feat: Central Trace Destination Opt-Out for Langfuse (#14838)
* feat(langfuse): let callers opt out of the central trace destination Adds `centralTraceExportEnabled` to the score-destination options and threads it through `getScoreDestinations`, `getLangfuseTraceDestinationIds` and `getLangfuseTraceMessageFields`. Deployments that route traces per tenant may want a given turn's spans to reach only the tenant destination — for example when central export is a per-tenant setting rather than a deployment-wide one. Today the central project is included whenever env credentials exist, with no way for a caller to decline it for a single trace. Defaults to `true` everywhere, so existing callers are unaffected: the option is additive and every current call site resolves exactly as before. The three public helpers gain an optional trailing parameter and nothing else. While here, `getScoreDestinations` destructures its options with defaults instead of repeating `options?.waitForCentralProjectId !== false` at both call sites, which is what made adding a second flag awkward. Verified: no new tsc errors (one pre-existing cacheFactory error is unchanged), 99 langfuse tests pass, ESLint and Prettier clean. * fix(langfuse): keep the central opt-out intact when destinations resolve Addresses two review findings on the new `centralTraceExportEnabled` option. Both are cases where opting out of central export was silently discarded by destination resolution, letting later feedback reach a project the trace never went to. 1. Non-fanout deployments with no central env credentials still returned the configured connection, because only the central-credential branch was gated. `resolveLangfuseExportPlan` reports `disabled` for that shape — without a fanout route there is nowhere for a central-suppressed trace to go — so return no destinations and match it. 2. `getLangfuseTraceDestinationIds` returned `undefined` whenever any destination lacked an id, and `sendFeedbackScore` reads `undefined` as unrestricted. A tenant destination has no id when its optional `projectId` is unset, so a suppressed-central trace could resolve back to the central project at feedback time. Fail closed with an empty list, which stays restricted, instead. Both paths now have regression tests, each verified to fail without the corresponding change: the first returned 3 destination ids, the second returned `undefined`. * fix(langfuse): carry the central opt-out into the feedback path `getLangfuseTraceDestinationIds` returns `undefined` when an eligible destination has no stable id, which a tenant route hits whenever the optional `langfuse.projectId` is unset. `sendFeedbackScore` read that as "unrestricted" and re-resolved destinations with central export enabled, so a suppressed-central trace still drew central feedback. Persisting an empty list instead only traded the leak for a drop: the destination filter rejects every id-less destination, discarding ratings the tenant should receive. The id list restricts feedback to destinations that survived reconfiguration; it cannot also encode deployment policy. Thread `centralTraceExportEnabled` through `sendFeedbackScore` so the policy is evaluated the same way at trace time and feedback time, and restore `undefined` for unidentifiable destinations. |
||
|
|
e1178d3c65
|
🏘️ fix: Scope OpenID User Cache Keys to Signed User Identity (#14837)
* fix(auth): scope OpenID user cache by tenant * fix(auth): preserve pre-auth cache scope * fix(auth): type OpenID reuse secret |
||
|
|
c06fbff475
|
📦 chore: bump @librechat/agents to v3.5.1 (#14830)
* 📦 chore: bump `@librechat/agents` to v3.5.0
* chore: bump agents sdk to v3.5.1
|
||
|
|
bc6392d05b
|
🪢 fix(langfuse): mark provider-backed agent traces (#14833)
* fix(langfuse): mark provider-backed agent traces * fix(langfuse): mark stored response traces * test(langfuse): isolate provider marker setup |
||
|
|
0ce4c3374b
|
⏲️ test: Give ServerConfigsDB Mongo Hooks a 60s Timeout Budget (#14831)
`beforeAll` boots a real mongod via MongoMemoryServer, resets the module registry and re-imports data-schemas, ServerConfigsDB and the MCP OAuth handler before a single test runs. That exceeds the 15s global `testTimeout` once the runner is busy: the suite finishes in ~4.5s on its own but has been observed at 16.8s under a loaded `@librechat/api` shard, failing every test in the file with "Exceeded timeout of 15000 ms for a hook". Give both mongo hooks an explicit 60s budget, matching `checkpointer.integration.spec.ts`, the other MongoMemoryServer suite that already opts out of the global default. `afterAll` gets the same treatment since `mongoServer.stop()` is subject to the same contention. No behaviour change — the timeout only bounds setup, and the suite still completes well inside it. |
||
|
|
eaef87fa26
|
🚀 chore: Prepare v0.8.8-rc1 (#14394)
* 🚀 chore: Prepare v0.8.8-rc1 release * 📚 docs: Complete v0.8.8-rc1 operator references * 📚 docs: Mark stateful sessions experimental * 📚 docs: Clarify background code capability * 📚 docs: Refresh v0.8.8-rc1 operator guidance * 📚 docs: Highlight v0.8.8-rc1 features in README * 📦 chore: Bump publishable packages again * 📚 docs: Add streaming question progress * 📦 chore: Bump publishable packages again * 📚 docs: Refresh v0.8.8-rc1 release highlights * 📦 chore: Bump publishable packages again * 📚 docs: Refresh v0.8.8-rc1 release guidance * 📦 chore: Bump publishable packages again * 📚 docs: Highlight batched Agent questions * 📦 chore: Bump publishable packages again * 📦 chore: Bump publishable packages again * 📦 chore: Bump publishable packages again * 📦 chore: Refresh v0.8.8-rc1 package versions * 📦 chore: Refresh v0.8.8-rc1 package versions * 📦 chore: Refresh v0.8.8-rc1 package versions * 📄 docs: Note PowerPoint template support * 📦 chore: Refresh v0.8.8-rc1 package versions * 📄 docs: Note latest provider and file support |
||
|
|
2f0cd2eb75
|
🔌 chore: Bump the MCP SDK to 1.30.0 and Parse Content-Type Instead of Searching It (#14820)
`@modelcontextprotocol/sdk@1.30.0` is a small maintenance release on the 1.x line (upstream's active line is now the 2.0.0 scoped packages). The range was already `^1.29.0`, so only the lockfile pinned the old version; the manifests move too so the floor matches what we test against. Nothing in it is breaking. The four changed type declarations are additive — optional `maxBufferSize` on `StdioServerParameters`, an optional third constructor argument on `StdioServerTransport`, optional options on `ReadBuffer`, optional `keepAliveMs` on the server transport — and the only manifest change is `@hono/node-server` widening to `^1.19.9 || ^2.0.5`. No new dependencies. Two behavior changes are worth knowing about even though neither is an API break. `ReadBuffer` now caps a single stdio message at 10 MB (previously unbounded) and errors the transport instead of growing, which is reachable through `StdioClientTransport` if a stdio server returns a very large single result; it takes `maxBufferSize` if that ever needs raising. And Content-Type handling switched from substring search to parsed media types, client and server. Most of the release is Streamable HTTP server hardening we do not run — a 15s SSE keep-alive, `X-Accel-Buffering: no` on SSE responses, guards so a stale stream's cancel cannot tear down its successor, and `_closed` checks so a transport closing mid-request stops registering streams into swept maps. None of it changes how we behave as a client. In particular it does not address the stale-stream 409 in #14816: that keep-alive runs in whichever server we connect to, not here. The same substring-vs-parse mistake the SDK corrected exists in our streamable HTTP response guard, which classified a response as SSE with `contentType.includes('text/event-stream')`. A `Content-Type` naming the SSE type in a parameter — `text/plain; boundary=text/event-stream` — is not an event stream, but matched. The guard then took `canEmitFallbackSSEError`, so an oversized body was answered with a synthetic SSE error frame the caller reads as a well-formed response body, rather than the throw a non-SSE response gets. The check now compares the parsed media type, via a `mediaTypeEssence` helper added to the header utils where `mergeHeaders` already lives. Verified against 1.30.0 rather than assuming: the package was staged into the worktree's own `node_modules` so it shadowed the shared install, and `packages/api` `src/mcp` ran green on it — same four pre-existing red suites as on 1.29.0 (`MCPReinitRecovery` plus three Redis `cache_integration` suites that need a live Redis), no new failures. |
||
|
|
24d111fde9
|
⚡ feat: Add Gemini 3.7 Flash Support (#14818)
* ⚡ feat: Add Gemini 3.7 Flash Support Adds first-class support for Google's Gemini 3.7 Flash (`gemini-3.7-flash`) for both the Gemini API (AI Studio) and Google Cloud Gemini Enterprise Agent Platform, following the Gemini 3.6 Flash integration (#14369). - Context window (1,048,576) in googleModels; API + cache pricing in tx.ts. - Model dropdown (config.ts) and GOOGLE_MODELS examples for both integrations. - Register the model in the Flash-family handler so it inherits the existing strip of deprecated sampling params (temperature/topP/topK), rejected penalty params, and thinkingBudget, and defaults to `medium` thinking. - Generalize that handler's enumerated table from a [id, level] tuple to a rule object, so a model can also declare thinking levels it rejects. Gemini 3.7 Flash errors on `minimal` (which the Google endpoint offers in its thinkingLevel slider), so an explicit `minimal` is substituted with the nearest supported level, `low`. Explicit low/medium/high pass through unchanged. - Apply Google's introductory pricing ($0.75 in / $3.75 out / $0.075 cached, per 1M) to Gemini 3.7 Flash and correct Gemini 3.6 Flash to the same rates. Both revert to $1.50 / $7.50 / $0.15 on 2027-01-01; noted at both call sites. Resolves #14802 Ref: https://ai.google.dev/gemini-api/docs/models/gemini-3.7-flash Ref: https://ai.google.dev/gemini-api/docs/pricing * 📝 docs: Match the House Style for Promotional Rate Comments Align the Gemini 3.6/3.7 Flash introductory-pricing notes with the existing Sonnet 5 convention in the same file: one comment per group, naming the models and the exact values to restore, so the manual follow-up is unambiguous. No rate changes. * ⬆️ chore: Bump `@librechat/agents` to 3.4.7 for Gemini 3.7 Flash Prefill Unblocks this PR. `NO_PREFILL_GEMINI_MODELS` is model-enumerated in the agents SDK, so 3.4.6 does not know `gemini-3.7-flash` forbids a trailing `model`-role turn — editing an assistant reply and resubmitting would reach Google as a prefill and return HTTP 400 on a model this PR adds to the default list. 3.4.7 (danny-avila/agents#412, released via #413) adds it. Verified the published tarball: `3.4.6...3.4.7` touches only `dist/{cjs,esm}/llm/google/utils/common.*` — the prefill array and its comment. `dist/types` is byte-identical, so there is no API surface change. Raises the declared range in both workspaces alongside the lock. `^3.4.6` already permitted 3.4.7, but the fix is required rather than merely compatible, so the floor should say so. |
||
|
|
5e464bc930
|
📎 fix: Alias Shell Script MIME Variants to application/x-sh (#14817)
* 📎 fix: Alias Shell Script MIME Variants to `application/x-sh` Chrome on Linux reports `.sh` files as `application/x-shellscript` (freedesktop shared-mime-info) and libmagic reports `text/x-shellscript`. Neither string appears anywhere in the source, so uploads were rejected even though `application/x-sh` is in the default allowlist and `codeTypeMapping` maps `sh` to it — `inferMimeType` only consults the extension map when the client sends no type at all, so a non-empty browser value passed straight through to the allowlist check. Alias both variants to the canonical `application/x-sh`, matching the existing treatment of `text/x-markdown` and `application/x-zip-compressed`. Also attach `statusCode`/`body` to multer file-filter rejections. Without them the error misses the `isCustomError` branch in `ErrorController` and falls through to a bare `500 An unknown error occurred.`, so the rejection reason was logged server-side but never reached the client. The upload hook already surfaces `error.response.data.message`, so a rejected file now explains itself instead of showing a generic upload failure. * 🔁 refactor: Move Upload Error Contract Into `packages/api` Addresses codex P1 on #14817. The producer of the `statusCode`/`body` pair now sits beside its consumer: `isCustomError` and `ErrorController` are already in `packages/api/src/middleware/error.ts`, and `CustomError` is already in `packages/api/src/types/error.ts` — only the construction of that pair was stranded in legacy JS. `createCustomError` is exported from the same module as the guard that recognizes it, and `multer.js` is back to a thin caller. Also pins the `.sh` back-compat claim with tests: configs from the documented workarounds (`application/x-sh` per #4660/#5689/#6297, and the broad patterns from #14804) still accept a `.sh` upload after the alias rewrites the type. A negative control confirms the endpoint config is genuinely in play rather than falling back to the default allowlist. |
||
|
|
6c46fd1252
|
📄 feat: accept PowerPoint template MIME type (#14761) | ||
|
|
6cbfd82772
|
🔌 fix: Recover Quietly From Stale MCP SSE Stream Conflicts (#14816)
A Streamable HTTP server allows one standalone `GET` SSE stream per session and
releases its mapping from the response stream's cancel callback. That callback
never runs when the connection dies at a proxy rather than at the client, so the
server keeps holding a stream nobody is reading while the client knows its stream
is gone. Every reconnect carrying that session id then gets a 409:
SSE stream disconnected: TypeError: terminated
Transport error (may require manual intervention):
Streamable HTTP error: Failed to open SSE stream: Conflict
Transport error (may require manual intervention):
Maximum reconnection attempts (2) exceeded.
Nothing there requires manual intervention. The connection recovers on its own in
a few seconds, because the rebuild the first 409 escalates to sends the
spec-mandated `DELETE`, which drops the server's session along with the stream it
leaked. Two things made a self-healing event read as a fatal one.
`extractSSEErrorMessage` classified status by scanning the message text for
digits, but `StreamableHTTPError` and `SseError` carry the status on `code` and
their messages do not always repeat it. "Failed to open SSE stream: Conflict"
has no digits at all, so a 409 never reached the status branch and fell through
to the terminal `isTransient: false` — the same verdict as a DNS typo. A 5xx
arriving on `code` alone had the same blind spot. The status is now read from
`code` when it is in HTTP range, with the message scan kept as a fallback, and
409 joins 5xx as transient: the stale session it reports is cleared by the
rebuild, with nothing for an operator to do.
The second is volume. Each SDK retry fires `onerror` twice — once with the raw
throw out of `_startOrAuthSse`, once with the `Failed to reconnect SSE stream`
wrapper. Only the wrapper matched the existing suppression, so every doomed retry
logged at error level, and the retries are doomed by construction: nothing about
the same session id can stop conflicting. The first conflict now escalates for
rebuild and the rest are logged as the echo they are, along with the SDK's
out-of-retries announcement when a rebuild is already underway. The non-conflict
path for that announcement is untouched, so an exhausted budget still falls
through to our reconnection everywhere else.
`extractSSEErrorMessage` moves to `errors.ts` alongside `isOAuthAuthenticationError`.
It had no test: `MCPConnection.test.ts` held a hand-copied clone marked "keep in
sync with the actual implementation", so 66 assertions were exercising the copy.
The clone is deleted and the suite now imports the real function, which it turns
out had not drifted.
`MCPConnectionSseConflict.test.ts` drives a real client transport against a real
in-process `StreamableHTTPServerTransport` reproducing the sequence above: the
stream opens, its socket is destroyed underneath the client, and every later
`GET` on that session id conflicts while a rebuilt session gets a healthy stream.
|
||
|
|
0654efb7ed
|
🔌 fix: Preserve MCP serverInstructions Declaration Through Inspection (#14815)
`MCPServerInspector` overwrote the operator's `serverInstructions` declaration with the text fetched from the server. That made a YAML server's cached entry differ from its own raw config on an admin-configurable field, so `isUnmodifiedYamlServer` misclassified it as admin-modified and re-inspected it on the first user-scoped resolve. The second inspection produced a config with a newer `updatedAt`, which: - flipped `getServerConnectionStatus` to `disconnected` permanently, since the healthy app connection was then measured against the newer timestamp; and - made `isAppServerConfig` reject the effective config, gating off the app connection so `GET /api/mcp/tools` returned zero tools and cached nothing. Fetched instructions now land on a separate `resolvedInstructions` field, matching how every other inspector-derived value is stored, so the declaration survives inspection and the guard compares like with like. Bumps `REGISTRY_STORAGE_SCHEMA_VERSION` so Redis-backed deployments rewrite entries whose `serverInstructions` still holds fetched text. Fixes #14798 |
||
|
|
abc669ab58
|
🩹 fix: Restore the @librechat/api Build and Remove Legacy Code (#14808)
* 🧹 chore: Remove Dead Legacy Agent Controller `_LegacyAgentController` has been unreachable since the resumable path became the only route: it is unreferenced, unexported, and untested. It had also drifted out of compilability against the live file — line 2009 called `attachConversationCreatedAt(req, { userId, conversationId, isNewConvo })` against the 3-argument signature declared at line 97, which would await `undefined` and then throw dereferencing `resolved.createdAt`. Keeping it was not free. It carried a third independent copy of the response message-id wiring (`getReqData`, `onStart`, four `updateMetadata` calls), so every change to how a generation identifies its response row had a dead third site to keep in step, and no test to say whether it had been kept in step. Removing the block leaves `createCloseHandler` and the `sendEvent`, `clientRegistry`, `requestDataMap` and `handleAbortError` imports with no remaining callers, so those go too. `AgentController` was a three-line passthrough to `ResumableAgentController`; the real controller is now exported directly, which also matches the `[ResumableAgentController]` prefix every log line in the file already uses. `server/routes/agents/chat.js` binds the export to its own local name and passes the same five arguments, so the route is unchanged. No behavior change: 379 lines removed, 2 added. * 🩹 fix: Remove Duplicated Anchor Block Breaking the `@librechat/api` Build `dev` does not build. `packages/api/src/agents/activityPhases/runtime.ts` carries two byte-identical 98-line copies of the same block (former lines 516-613 and 614-711), so rolldown fails to parse it: [PARSE_ERROR] Identifier `AnchorFields` has already been declared The duplicated block is the anchor-construction work from #14805: `AnchorFields`, `laterDefinedIndex`, `foldedAgentIds`, `boundedAnchor` and `mergeAnchors`. #14807 was squashed from a branch that predated #14805 and re-included that commit, so both copies landed. Only the `type` produced an error — the four function declarations simply redeclare. This removes the first copy. The two blocks were verified byte-identical before the cut, and the resulting file has no duplicate top-level declarations, is missing nothing that #14805 introduced, and retains everything new to #14807 (`ResolvedPosition`, `resolvePosition`). Verified: `tsdown` builds, `tsc --noEmit` clean, `config/circular-deps.mjs` green across all five graphs (it was reporting `✗ @librechat/api` purely because the build it shells out to was failing), and the 68 tests in `activityPhases/runtime.spec.ts` pass. Carried here rather than in a separate PR because this PR's checks cannot go green until it lands: the failed `packages/api` build cascades into e2e, MCP list_changed, bombadil and the Docker image jobs. |
||
|
|
78eb0c98ce
|
🧭 refactor: Resolve Activity Phase Position Once Per Boundary (#14807)
* refactor(api): make anchor construction exhaustive Bounded anchors were built in two places, each spreading one side and hand-picking the rest, so any field nobody named was dropped silently and nothing failed until a boundary landed badly. That already cost agentId and then unresolvedToolStartIndex in consecutive review rounds, and mergedAgentIds was never added to the demotion path at all. Both constructors now assign an AnchorFields literal, mapped over keyof Required<TrackedActivity>, so adding a field to TrackedActivity is a type error at both sites until its anchor semantics are decided. No behavior change: every field resolves to what the hand-picked versions already produced. Adds a folding property over count, failure count, agent attribution, and ordering. * refactor(api): resolve activity position once per boundary Positional fields on TrackedActivity are captured at different times against an array that keeps moving, so each was a cache that could go stale, collide, or be truncated — and closesBeforeBoundary read three of them directly. Roughly two thirds of the review findings on #14785 were that pattern: a proxy outranking, outliving, or standing in for the rendered position. resolvePosition now folds tool indices, the prior partition floor, the unmaterialized fallback, and reasoning anchors into one value, and closesBeforeBoundary takes only that value. A caller cannot reach past it to a raw field, and the four branches the predicate used to carry collapse into one comparison: an activity closes early exactly when nothing locates it beyond the boundary. Resolution also decides when the saved fallback has gone stale, so the stripping that kept it out of snapshots is now a property of the resolved value rather than a separate step. No behavior change; the existing boundary and straddle regressions cover both directions. |
||
|
|
619ed2f1fb
|
🧱 refactor: Make Activity Phase Anchor Construction Exhaustive (#14805)
Bounded anchors were built in two places, each spreading one side and hand-picking the rest, so any field nobody named was dropped silently and nothing failed until a boundary landed badly. That already cost agentId and then unresolvedToolStartIndex in consecutive review rounds, and mergedAgentIds was never added to the demotion path at all. Both constructors now assign an AnchorFields literal, mapped over keyof Required<TrackedActivity>, so adding a field to TrackedActivity is a type error at both sites until its anchor semantics are decided. No behavior change: every field resolves to what the hand-picked versions already produced. Adds a folding property over count, failure count, agent attribution, and ordering. |
||
|
|
05ed7ad8c0
|
🔖 fix: Split Activity Phases at Substantial Text (#14785)
* fix(api): split phases at substantial text * tune(api): split phases after 200 text chars * fix(api): reanchor substantial text boundaries * test(api): type multi-phase payload captures * fix(api): preserve activity phase boundaries * fix(api): anchor retained phase partitions * fix(api): persist phase partition anchors * fix(api): harden activity phase boundaries * fix(api): preserve bounded phase partitions * fix: preserve final and delayed phase content * refactor(api): partition phase state at one boundary Boundary closure split fifteen separately-maintained fields by hand, and each fix partitioned one more while the next stayed unguarded. Fold the overflow bookkeeping into the tracked activity list so every counted activity carries a position, and route the split through a single partitionAt that returns both sides. Counts are now summed from the partition instead of reconstructed by subtraction, so a run past the anchor budget reports every activity it performed rather than the truncated window. Snapshots move to version 3; the reader still accepts versions 1 and 2 and rebuilds their unpositioned remainder as a bounded anchor, dropping it when its evidence is stale. Adds a boundary-conservation property covering every split point. * fix(client): drop empty phase content segments Late-child recovery can strip every index from a segment it already claimed, leaving a content segment with no parts. Each one still mounts a nested ContentParts that renders nothing, and it broke the exact-segment expectation in the late-child regression from |
||
|
|
bcbe26ab4c
|
🪑 fix: Rebase Activity Phase Bounds Onto Compacted Content and Unskip the MCPManager Suite (#14782)
* 🧭 fix: Rebase Activity Phase Bounds Onto Compacted Content `filterMalformedContentParts` compacts the aggregator's content array — `Array.prototype.filter` skips holes and drops malformed tool calls — but a parent phase marker's `activity_start_index`/`activity_end_index` still address the pre-filter positions. The array is routinely sparse: the aggregator writes parts at provider-source indexes, so a model turn that emits no text before its tool calls leaves an empty slot. Every part after a hole therefore shifts left on persistence while the bounds stay put, so the stored phase claims the wrong range — the final answer is swallowed into the parent card and the marker's own slot is counted as a child. The in-run analogue (`rebaseActivityPhaseBounds`) already rebases after completion-time reshaping; the final compaction had no such step. Rebase the bounds as part of the compaction, mapping each bound to the number of retained parts ahead of it. The mapping is monotonic, so `start <= end <= markerIndex` survives, and an identity mapping leaves untouched arrays — and their marker objects — exactly as they were. Markers are copied rather than mutated so the caller's array keeps its own coordinates, which the live stream and the resume snapshot still address. Fixes the `activity-phases` e2e failure on dev and the same defect on the two resume persistence paths. * 🔌 fix: Stop Replacing the Env Module in the MCPManager Suite `MCPManager.test.ts` mocked `~/utils/env` with a factory that replaced the whole module. #14780 then made `~/mcp/utils` read `ALLOWED_BODY_FIELDS` from that module at module scope, so importing `~/mcp/oauth` -> `handler.ts` -> `~/mcp/utils` evaluated `undefined.map(...)` and the suite died at import time. All 111 of its tests have been silently skipped since; the shard has been red on dev, on this PR, and on release-v0.8.8-rc1. Spread the real module and keep only the mock that earns its place. `processMCPEnv` stays a seam: fifteen cases drive it with `mockReturnValue` / `mockImplementation` to hand the manager a specific processed config, and one asserts its call count, so making it real would couple these tests to env-substitution logic. `isPluginSourced` and `MCP_PLUGIN_SOURCE` were dropped — the factory restated the real implementations verbatim and no test referenced either, so they were duplication, not a seam. 111 tests now run and pass. * 🧪 test: Stop Replacing the Env Module in Three More Suites Same latent trap as the MCPManager suite: a `jest.mock('~/utils/env', ...)` factory that replaces the whole module. These three pass today only because their import graphs never reach `~/mcp/utils`, which reads `ALLOWED_BODY_FIELDS` from that module at module scope — the next module-scope constant added to `env.ts` would break all three the same silent way. Each mock is kept only where it earns its place: - `activityLabels/host.spec.ts` — dropped. `createSafeUser` was never referenced and the stub returned `undefined` where the real function returns `{}`, so the mock was strictly less faithful than the real, pure implementation. - `run-codeTools.test.ts` — dropped. Neither `resolveHeaders` nor `createSafeUser` was referenced by any case. - `run-summarization.test.ts` — `resolveHeaders` is now a spy wrapping the real implementation rather than an identity stub. One case asserts templated header values go through it, which only means something if the real substitution actually runs. `createSafeUser` dropped as unreferenced. 103 suites / 2708 tests green across `src/agents`, `src/utils`, and the MCPManager suite. * 📝 docs: Describe the Full Contract of filterMalformedContentParts Per Copilot's review: the public JSDoc still described the function as only dropping malformed tool calls, while the implementation also compacts empty slots and rebases parent activity-phase bounds. The detail lived on the private helper, so callers reading intellisense saw a stale contract. State what it actually produces, note that compaction is inherent rather than incidental (the aggregator writes at provider-source indexes, so the array is frequently sparse), and add an example of a hole moving a phase bound. The example was verified against the built runtime, not written from memory. |
||
|
|
df6e15a0de
|
🔖 feat: Bound Parent Activity Phases With an Exclusive End Index (#14768)
* 🧭 fix: Finalize Parent Activity Phases at Run Completion * 🧭 fix: Preserve Activity Phase Boundaries * 🎨 fix: Format Activity Phase Boundary Check * 🧭 fix: Ignore Late Label Artifacts at Phase Completion * 🧭 fix: Preserve Logical Activity Phase Membership * 🩹 fix: Narrow Optional Activity Phase Marker * fix activity phase tail boundaries * fix activity phase test lint * fix straddling activity phase batches * preserve activity phase boundaries at scale * fix persisted activity phase final boundary * fix resumed activity phase edge cases * fix sparse activity phase grouping * fix sparse activity phase tail scan * fix resumed activity phase text fallback * fix sparse activity phase completion scans * avoid sparse activity phase runtime scans * stabilize sparse activity phase resumes * support activity phases on current ts target * preserve sparse phase reservations * finalize activity phase boundary handling * avoid sparse phase start scans * fix activity phase final text bounds * tighten activity phase summary boundaries * format activity phase boundary checks * leave final commentary outside activity phases * recognize lane-tagged final activity text * rebase retained activity boundaries on resume * bound activity phase collection work * correct resumed phase activity count * resolve late reasoning before phase completion * preserve lane-tagged final answers * assert durable activity phase bounds in e2e * preserve empty finalized activity phases * ignore empty reasoning at phase completion * format phase completion guard * fix(api): retain overflow reasoning anchors * perf(api): index overflow reasoning anchors * perf(api): skip empty reasoning index scans * fix(api): reconcile completion boundaries efficiently |
||
|
|
1a3e2aebcb
|
🛰️ fix: Attach Request-Scoped MCP Servers (#14780)
* fix: attach request-scoped MCP servers * fix: satisfy MCP static checks * fix: format MCP runtime hint |
||
|
|
298a3d9ee9
|
📦 chore: Update @librechat/agents to v3.4.6 (#14781)
|
||
|
|
8f1f961212
|
🧱 refactor: Require Broad Config Management for Base Field Mutations (#14775) | ||
|
|
861cfe8a3c
|
🧩 fix: Normalize Malformed MCP Required Schemas (#14771) | ||
|
|
9980b6221f
|
🪢 feat: add Langfuse session links (#14776)
* feat: add Langfuse session links * fix: tighten Langfuse session link resolution * fix: clear stale Langfuse session links * test: verify tenant Langfuse session links * fix: align Langfuse link with client conventions |
||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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. |