Commit graph

613 commits

Author SHA1 Message Date
Marco Beretta
318da18d28
Fix canonical artifact ownership and frontend review regressions 2026-09-06 18:46:50 +02:00
Marco Beretta
5f9315e944
Resolve follow-up review findings 2026-09-06 18:46:50 +02:00
Marco Beretta
ea6e72fc7b
fix: address remaining Codex findings after dev rebase
Stamp agent ownership on streaming and final search snapshots. Compose the shared floating thought button. Preserve rebased group behavior, correct stale test selectors, and keep expansion callbacks outside state updaters.
2026-09-06 18:46:50 +02:00
Marco Beretta
f0f5bdd3a0
fix: persist failed summaries and stop double-rendering memory mutations
Stamp the failed marker on the aggregated summary part server-side, mirroring
how ON_RUN_STEP_CLOSED already stamps runStepStatus. The SDK aggregator ignores
a complete event that carries no summary, so an errored round's partial deltas
were saved unmarked and a reload re-rendered the truncated text under
"Conversation summarized".

Skip memory attachments that carry a toolCallId in the message-level
MemoryArtifacts decoration. Those calls already render inline as a MemoryCall
card with the same key, value and outcome, so one mutation appeared twice.
Attachments with no originating call keep the decoration as their only surface.

Keep a reasoning-bearing group expanded when Open Thinking Dropdowns by Default
is on. The folded-in THINK part only renders inside the group body, which is
not even mounted while collapsed, so the preference had no visible effect until
the action group was opened by hand.

Give same-named place results unique keys. Several branches of one chain share
a name and often carry no identifier, so rows whose address, rating and map
target all differ collided on one React key.
2026-09-06 18:46:10 +02:00
Danny Avila
aeac7a2065
🪪 feat: Add Per-Agent Git Identity (#15628)
* feat: Add per-agent Git identity

* fix: annotate git identity schema

* fix: complete git identity controls

* fix: clear git identity on version restore

* fix: Require a complete agent Git identity

* fix: Complete Git Identity Across Bash And Form Validation

* fix: Discard Incomplete Hidden Git Identity Edits

* fix: validate Git identity across panel lifecycle

* fix: preserve saved Git identity when discarding drafts
2026-09-05 23:56:23 -04:00
Ravi Kumar L
000349681e
🩻 feat(insights): Scope Insights to Authorized Agents (#15549)
Some checks failed
Docker Dev Images Build / publish (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
* feat(insights): add agent-scoped access

* fix(insights): exclude unattributed assistant messages

* fix(sharing): key role insight toggles by principal

* fix(sharing): preserve insight grant snapshot

* fix(permissions): preserve insight bits atomically

* fix(permissions): guard insight audit rollback

* fix(insights): keep admin grant controls accessible

* fix(permissions): reconcile duplicate principal updates

* fix(insights): show automatic admin access

* fix(permissions): harden insights access updates

* fix(insights): simplify authorization and preserve client state
2026-09-04 20:16:58 -04:00
Danny Avila
5e3cdd0b2c
📮 fix: Consume an Invite Only Once the Account Exists (#15611)
* 📮 fix: Consume an Invite Only Once the Account Exists

`checkInviteUser` deleted the invite token and then called `next()`, but everything
that can still reject a registration runs after it: the schema, the allowed-domain
check, and the email-already-in-use check. A mistyped password confirmation therefore
destroyed the invite — the invitee corrected it, resubmitted, and got "Invalid invite
token" with no way back short of an admin re-inviting them.

The deletion moves to `registrationController`, after `registerUser` reports success.

That report needed a new signal. `registerUser` returns the same 200 and the same
generic message whether it created an account or found the email already in use —
deliberately, so the response cannot be used to enumerate accounts — so the status
alone cannot say whether an account exists. It now also returns `userCreated` on the
creation path, which the controller reads and never forwards; the response body is
unchanged.

A failed deletion is logged rather than surfaced. By that point the account exists,
and leaving a usable invite behind is recoverable in a way that telling the user their
registration failed is not.

Fixes #15541

* ♻️ test: Fold the Invite Tests Into the Existing AuthController Spec

From Copilot's review. The new spec reached for `jest.requireActual` on
`@librechat/data-schemas`, `~/server/services/AuthService` and `~/models`, which pulls
real implementations into a unit test — `~/models` builds the data-schemas methods
against mongoose — and diverges from `AuthController.spec.js`, which stubs each module
outright.

Rather than restate that harness with narrower stubs, the tests move into the spec that
already has it. `deleteTokens` joins its `~/models` mock, which also keeps that shared
mock in step with the controller's imports: a stale mock there hands the controller an
undefined function, and the omission only surfaces when some later test happens to
exercise the path.
2026-09-04 15:38:32 -04:00
Danny Avila
770238f22b
🔣 refactor: Replace Mongo Filters With Typed Criteria (#15580)
* ♻️ refactor: Take Typed Criteria, Not Mongo Filters, for Actions and Assistants

`createActionMethods` and `createAssistantMethods` accepted `FilterQuery<T>`,
putting Mongoose's query language in the package's public surface: every caller
wrote raw Mongo, and no engine other than Mongo could satisfy the signatures.

Both now take domain criteria — `ActionQuery` and `AssistantQuery` — naming
concepts rather than stored fields, with an array meaning "any of these". A
per-domain field map translates them, so `FilterQuery` survives only inside the
two translators.

`buildFilter` throws on a criterion the field map does not cover. Callers in
`api/` are JavaScript and get no compile-time checking, and the server sets
`strictQuery`, under which a silently dropped criterion widens the filter
instead of narrowing it — an unscoped `findOne` returns an arbitrary document
rather than none. Failing closed is the only safe default.

`AssistantQuery.avatarFilepath` covers the avatar-authorization lookup, which
queries a nested path no plain field name reaches.

No behaviour change: every migrated call site produces the same filter as before.

* 🧪 test: Update Avatar Authorization Assertion to Typed Criteria

`validateImages.spec.js` asserted the old `{ 'avatar.filepath': { $in: [...] } }`
call shape for `getAssistant`. The earlier sweep matched on `action_id`,
`agent_id` and `assistant_id`, none of which appear in a dotted avatar path, so
this one assertion was missed.

The `getAgent` assertions in the same file keep the filter shape — that method
still takes a `FilterQuery`.

* 🔒 fix: Reject Unknown Criteria Before Omitting Undefined Ones

`buildFilter` skipped a criterion whose value was `undefined` before checking it
against the field map, so a JavaScript caller misspelling a key that happened to
carry `undefined` — `deleteActions({ agent_id: maybeId })` — produced `{}` and
reached `deleteMany` unscoped. That is the exact failure the guard exists to
prevent, defeated by the order of the two checks.

Validate every key first, then apply the undefined-omission rule. The lookup is
an own-property check so inherited names cannot resolve through the field map's
prototype: `{ toString: 'x' }` previously found `Function.prototype.toString`,
passed the truthiness test, and wrote a garbage filter key instead of throwing.

A recognized criterion left `undefined` is still omitted, so optional parameters
keep working.

* 🔒 fix: Reject Query Fragments as Criteria and Keep the Translator Internal

Three review findings on the criteria translator, all of the same shape: the
guard was narrower than the surface it protects.

Criterion values are now validated as scalars or lists of scalars. `matchAny`
copied any non-array through unchanged, so a JavaScript caller could pass
`{ agentId: { $ne: null } }` and have the operator land verbatim in the filter
— the fail-open case the field map exists to close. `null` is rejected too; it
would have matched documents missing the field entirely.

`matchAny`, `buildFilter` and `FieldMap` are no longer re-exported from the
package barrel. They emit Mongo, and shipping them as public API undercut the
point of the change; they were reachable as `require('@librechat/data-schemas')
.buildFilter`. `OneOrMany` moves to `types/query` because the domain query types
reference it and it names no engine — which also drops the `types` -> `utils`
import.

`loadActionSets` names `ActionQuery` through `import()` so the JSDoc type
actually resolves.
2026-09-04 11:54:21 -04:00
Marco Beretta
10c5c91594
🎨 feat: Theme-Adaptive SVG Support for Custom MCP and Group Icons (#13817)
* feat: theme-adaptive SVG support for custom MCP and group icons

Custom icons (MCP server iconPath, model spec groupIcon) were rendered as
plain <img>, so monochrome SVGs kept fixed dark colors and were nearly
invisible in dark theme.

Introduce a shared CustomIcon component that detects monochrome SVG glyphs
and tints them with currentColor so they follow the active theme, while
multi-color SVG logos and raster images keep their original colors. The
monochrome decision parses the SVG's color tokens; content is fetched once,
cached, and any failure falls back to the original image. Monochrome SVGs
render via CSS mask, never inlined, so no SVG markup reaches the DOM.

Apply across all custom-icon surfaces: MCP settings cards, the chat MCP
dropdown, stacked MCP icons, tool-call headers, and model group icons.

Also support SVG in the MCP avatar uploader: add SVG to the accepted file
types and sanitize uploaded SVGs with DOMPurify before storing them, and
make the dialog preview theme-adaptive via the same component.

Add unit tests for SVG detection, monochrome analysis, sanitization, and
CustomIcon rendering.

* fix: reset adaptive icon tint state when the source changes

A reused CustomIcon instance kept the monochrome verdict from a previous
source until its effect re-ran, so switching to a raster image or a new
multi-color SVG could briefly render it as a currentColor silhouette.

Key the resolved verdict to the current source and reset it synchronously
during render (seeding from cache when available), so a stale verdict can
never tint a different icon. Add hook tests covering the source change.

* fix: do not tint custom SVGs that have an opaque background

A grayscale SVG with an opaque full-canvas background (for example an
exported logo with a white background rect and a black glyph) passed the
monochrome check and was drawn through a CSS mask. Masks key off the alpha
channel, so the opaque background filled the whole area with the tint color
and the icon collapsed into a solid block.

Detect a full-canvas opaque background rect and exclude such SVGs from
tinting, rendering them with their own colors instead. Transparent
single-color and multi-shade glyphs remain tintable. Add tests for the
background cases.

* fix: detect opaque SVG backgrounds without a viewBox

Opaque background detection only read canvas dimensions from the viewBox, so
an SVG that declares width and height on the root element but omits the
viewBox slipped through and was tinted into a solid block.

Fall back to the root svg width and height when no viewBox is present, and
match attribute names exactly so stroke-width is not mistaken for the canvas
width. Add tests for the no-viewBox cases.

* fix: parse comma-separated SVG viewBox values

The viewBox regex only accepted whitespace between values, so a valid
viewBox like "0,0,24,24" failed to parse and an opaque background went
undetected, tinting the icon into a solid block. Accept commas and
whitespace as separators. Add a test for the comma-separated case.

* refactor: detect SVG tintability with DOMParser instead of regexes

The monochrome/tintable decision scraped SVG markup with regexes, which kept
missing edge cases (opaque backgrounds, missing or comma-separated viewBox,
stroke-width vs canvas width, embedded raster images).

Parse the SVG once with DOMParser and inspect real elements and attributes:
reject embedded <image>/<foreignObject> content, detect a full-canvas opaque
background rect, read the canvas size from the viewBox or root width/height,
and gather paint colors from attributes, inline styles, and <style> blocks.
Unparseable input is treated as not tintable. Tests cover these cases.

* fix: only tint single-tone grayscale SVGs

A grayscale SVG can draw its background as a full-canvas path (not just a
rect), e.g. a white background path plus a black glyph. The rect-only
background check missed that, and the icon flattened to a solid currentColor
block under the CSS mask.

Tint only when the SVG resolves to a single grayscale tone. Any second tone
(a background shape drawn as a path or rect, an accent, or a second shade)
now preserves the icon's own colors, which covers full-canvas path
backgrounds without per-shape geometry parsing.

* fix: count default black fill when detecting tintable SVGs

* fix: treat currentColor as a tone and stroked closed shapes as filled

* fix: ignore non-rendering template paint and count filled open paths

* fix: scan defs and symbol paint rendered through use references

* fix: resolve currentColor against fixed color and match style fills for default paint

* fix: treat alpha-zero fills as transparent in SVG tint detection

* fix: resolve inherited and CSS fill for backgrounds and default paint

* fix: resolve CSS currentColor and default-black use instances

* fix: count default-filled polylines when detecting tintable SVGs

* fix: resolve inherited fill and style/CSS opacity in SVG tint detection

* fix: ignore display:none shapes and stop counting CSS color as paint

* fix: only count CSS paints that match rendered elements

* fix: match root for CSS currentColor and skip unreferenced defs tones

* fix: count paint only on rendered shapes and filter hidden CSS currentColor

* fix: resolve use-instance currentColor and skip hidden use default fills

* fix: resolve CSS paints onto rendered painters and skip overridden use fills

* fix: keep checking ancestor opacity after resolving paint opacity

* fix: resolve CSS paints by specificity and skip transparent default fills

* fix: apply CSS over presentation attributes and follow nested use targets

* fix: strip CSS comments, honor visibility, and inherit paint through nested use

* fix: skip transparent svg use references

* Account for SVG filter colors

* fix: propagate outer use color through nested currentColor references

* fix: preserve url() id casing and record every nested use chain

* fix: preserve SVGs whose filters recolor the source

* refactor: detect monochrome icons via canvas pixel sampling

Replace the SVG source parser with offscreen-canvas pixel sampling: draw the icon, read pixel data, and treat it as monochrome only when every non-transparent pixel is grayscale within tolerance and the image has some transparency (a fully opaque image would mask to a solid block). Verdicts are cached per src.

An explicit monochrome flag on CustomIcon skips detection; sampling is the fallback. Image load failures and canvas taint from non-CORS cross-origin icons fall back to untinted rendering instead of throwing.

* harden custom SVG icon handling across security, a11y, and perf

Sanitize user-provided MCP iconPath server-side; the client-side
DOMPurify pass was bypassable by posting iconPath straight to the API.
Adds sanitizeMcpIconPath in @librechat/api (allowlist SVG sanitizer that
preserves case-sensitive names) and runs it in the create/update
controllers, plus a length cap on iconPath in the shared schema.

- gate theme detection to same-origin/data sources so a remote icon is
  no longer auto-fetched from every viewer's browser
- restore forced-colors (High Contrast) visibility and the broken-icon
  onError fallback for tinted icons
- mark redundant icons decorative to avoid screen-reader double reads
- base64-encode inlined SVG data URIs and bound the verdict cache
- harden the client SVG sanitizer and guard FileReader failures
- cover the tinted render branch, sanitizer allowlist, and edge cases

* fix: preserve local SVG references while stripping external hrefs

The hardened sanitizers dropped <use> and every href outright, blanking
self-contained exporter output like <defs><path id="p"/></defs>
<use href="#p"/> and gradients inheriting stops via href="#g".

Re-allow <use> and href/xlink:href on both sanitizers, restricted to
same-document fragments: the client uses a dedicated DOMPurify instance
with an afterSanitizeAttributes hook (kept off the shared default
instance), and the server mirrors the rule with a sanitize-html tag
transform plus an empty allowedSchemes list as a second layer. External,
protocol-relative, relative-path, and javascript: hrefs are still
stripped, leaving the element inert.

* fix: keep xlink namespace and safe filters in the server icon sanitizer

The server allowlist stripped xmlns:xlink from the root while keeping
xlink:href, leaving an unbound prefix that is a parse error once the
icon is stored as an image/svg+xml data URI, so SVG 1.1 exporter output
went blank after save. It also dropped <filter>/fe* primitives that the
client sanitizer's svgFilters profile allows, so icons relying on
effects like feDropShadow lost them between preview and reload.

Allow xmlns:xlink and mirror the client's filter allowlist (elements
plus their presentation attributes); feImage passes through the existing
fragment-only href transform, so external references are still stripped.

* fix: require empty pixels for tinting and block external url() references

Two issues from review of the icon sanitizer and monochrome detector:

- scanMonochrome flagged any pixel below full opacity as transparency,
  so an SVG that fills the whole canvas with a semi-transparent grayscale
  wash was tinted and rendered as a solid currentColor block. Require a
  genuinely empty pixel (alpha at or below the paint threshold) instead.

- The fragment-only reference rule only covered href/xlink:href, so a
  url() in filter/fill/mask/clip-path/style kept external targets like
  filter="url(https://evil/f.svg#f)". Both sanitizers now strip any
  attribute carrying a non-fragment url(), preserving local url(#id).

* fix: preserve internal SVG stylesheets while scrubbing their references

Exporter SVGs that store multi-color paint in an internal <style> block
(e.g. .red{fill:#e00} with class="red") lost all styling because the
sanitizers dropped <style> outright, so multi-color logos rendered with
default paints after upload.

Re-allow <style> on both sanitizers and scrub its CSS: strip comments
and @import at-rules and rewrite any non-fragment url() to none, keeping
local url(#id) paints. These icons only render in inert <img>/CSS-mask
contexts isolated from the host page (no script, and no subresource
loads once external refs are scrubbed), so a script-free stylesheet is
safe to keep; a premature </style> close still has its trailing markup
re-sanitized by the parser.

* fix: normalize icon value before deciding it is not an SVG data URI

A posted iconPath with leading whitespace or C0 controls (e.g.
"\n data:image/svg+xml,...") slipped past the anchored data-URI check,
so sanitizeMcpIconPath returned it raw while a browser trims the prefix
and still renders the SVG, keeping its external <image>/url() and other
stripped content. Normalize the value the way URL parsing does, trimming
leading/trailing C0 controls and spaces and removing embedded
tab/newline, before the SVG check so these variants are sanitized too.

* fix: cap sanitized icon length and re-encode as compact base64

The schema length check ran on the input, but sanitizeMcpIconPath then
re-encoded the markup with encodeURIComponent, which expands SVG by well
over the base64 input size (a 235KB base64 icon became a 374KB percent
string). create stored that over-limit value, and editing the server
resubmitted the prefilled value and failed MCPServerUserInputSchema.

Re-encode the sanitized SVG as base64 (far more compact for angle-heavy
markup, which sanitizing can itself grow via explicit close tags) and
drop anything still over MAX_MCP_ICON_PATH_LENGTH, so a stored icon can
never exceed the cap that a later edit re-validates against.

* fix: resolve CSS escapes before stripping external SVG references

A literal url(/@import matcher missed CSS-escaped references: a value
like style="fill:u\72l(https://attacker/x)" or an escaped @import
survived sanitization because the browser un-escapes \72 to r at
tokenization time while the regex never saw a url(...). The external
reference was then persisted and served to other clients.

Un-escape CSS the way a browser does (single pass over \hex and
\char escapes) before the url()/@import matchers run, on both the client
and server sanitizers. Also scrub the multi-declaration style attribute
in place instead of dropping it, so co-located local paint rules survive
while escaped external references are neutralized.

* fix: re-sanitize after scrubbing style blocks to block markup reintroduction

scrubStyleBlocks runs after sanitizeHtml and splices un-escaped CSS back
as raw markup, so an escaped sequence like \3c/style\3e\3cimage/\3e
(inert text during the first pass) became a real </style><image href>
element after unescaping, past the allowlist. Re-run the allowlist over
the scrubbed result whenever a <style> block was rewritten, stripping any
element the un-escaping surfaced while leaving legit local rules intact.

The client sanitizer is unaffected: it sets the scrubbed CSS as a DOM
text node, which serializes with < and > escaped, so no element is
reintroduced. Added a regression test pinning that behavior too.

* fix: make forced-colors icon tint override the inline background color

The forced-colors rule for .custom-icon-tint had no !important, so it
lost the cascade to the element's inline `background-color: currentColor`
and the masked glyph still blanked out in Windows High Contrast — the
exact regression the rule was meant to prevent. Verified in a real
forced-colors render: without !important the tinted icon is invisible;
with it the glyph paints in CanvasText and stays visible.

* fix: enforce icon length cap in the sanitizer, not the schema

The schema `.max()` on iconPath rejected the whole update at parse time.
A server whose stored icon predates the cap (previous versions allowed
large data-URI images) re-submits that value from the edit dialog, so
the user was locked out of changing any field or clearing the bad icon.

Drop the schema `.max()` and enforce MAX_MCP_ICON_PATH_LENGTH in
sanitizeMcpIconPath for every value type: an over-cap SVG is compacted
or dropped, an over-cap raster/URL is dropped. Editing a server with a
pre-existing oversized icon now succeeds and clears the icon instead of
failing validation.

* fix: decode XML entities and parse quoted URLs in SVG CSS scrubbing

Two more bypasses of the CSS reference scrubber, both live once the
stored image/svg+xml is parsed by a viewer:

- A <style> block reaches the scrubber as raw text, so an entity-encoded
  reference like &#64;import (or &#x40;import) was never seen as @import;
  the browser decodes it at render time. Decode XML character references
  (numeric + the five predefined named) before the matchers run.
- The url() regex excluded ')' even inside quotes, so a valid quoted URL
  like url("https://x/a)b") slipped through. Parse quoted and unquoted
  URL tokens separately so a ')' inside quotes stays part of the target.

Applied to both the server (sanitizeMcpIconPath) and client (sanitizeSvg)
scrubbers. Entity decoding also catches an entity-encoded </style> markup
breakout, which the re-sanitize pass then strips.

* fix: close custom icon review threads

* fix: transpile sanitize-html's ESM dependency chain in api jest runs

* fix: preserve marker definitions and fragment-only marker references

* fix: keep object-cover sizing when tinting masked icons

* fix: preserve textPath labels and currentColor color scopes in icon sanitizer

* fix: preserve pattern coordinate-system attributes in icon sanitizer

Keep patternUnits, patternContentUnits, and patternTransform so patterned
SVGs do not fall back to objectBoundingBox after server-side sanitize.

* refactor: sanitize icons with one shared DOMPurify policy

The server re-implemented DOMPurify's SVG profiles as two hand-written
sanitize-html allowlists, so every element and attribute the client kept had to
be transcribed by hand, and each miss surfaced as an icon that previewed
correctly and then lost its markers, patterns, filters, or xlink binding once
stored. Run DOMPurify on jsdom instead and share the config and reference hook
with the client, which drops both allowlists.

Restrict url() values to same-document fragments on every attribute rather than
on the marker properties alone, so external references in fill, stroke, filter,
mask, and clip-path are stripped as well.

Restore feDropShadow casing after sanitizing. DOMPurify has to parse as HTML,
its XML mode drops filter primitives and gradients, and the HTML tag-name
adjustment table predates that primitive, so it survived lowercased and left its
filter empty once the stored icon was re-parsed as image/svg+xml.

* fix: defer jsdom load until MCP icon sanitization

jsdom and dompurify were imported at module scope, so every
require('@librechat/api') paid their startup cost. Load them on first
SVG sanitize instead.

* fix: sort type import in MCP icon sanitizer

The static-check import sorter wants value imports before type-only
imports. Moving the DOMPurify type import after librechat-data-provider
clears the drift.

* fix: address PR review bot findings

chatgpt-codex-connector:
- Resolve icon URLs through new URL() instead of trusting a leading slash, so
  /\attacker.example/icon.svg is no longer treated as same-origin
- Check every url() token in an SVG attribute rather than only the first, and
  reject values carrying a CSS escape that would decode into one
- Forbid animateTransform, animateMotion, animateColor, and mpath, which the
  SVG profile admitted despite the policy intending to remove animation
- Preserve the radial-gradient fr attribute the profile omits
- Reject multi-tone grayscale in scanMonochrome, which a mask would flatten
- Sample at 128px so a small chromatic accent is not averaged away
- Settle a stalled icon load after 10s so its in-flight entry is released
- Bound the decoded SVG before building a jsdom tree for it
- Reject an oversized icon upload with a visible error instead of accepting it
  and silently clearing it on save

* fix: address Codex findings and trim icon sanitizer comments

- Bound icon files by size before reading or sanitizing them
- Track tone spread across every painted pixel, not only solid ones
- Settle monochrome detection on any sampling throw
- Use named DOMPurify/JSDOM types and shorten PR comments

* fix: percent-decode SVG data URI bodies before base64 decoding

* fix: declare the SVG namespace on sanitized icons and keep icon rejections out of form validity
2026-09-04 10:49:43 -04:00
Dustin Healy
5840e2f2ae
🪓 feat: Add Tenant-Scoped Agent Deletion (#15573)
* feat: add Agent Management delete endpoint

* fix: scope agent deletion cleanup by tenant
2026-09-04 09:51:12 -04:00
Danny Avila
30cd1c2f2e
🔇 fix: Stop logging an absent create_file target as an error (#15591)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
`create_file` reads its target before writing so it can tell a create from
an overwrite, so `readSandboxFile` missing the path is the ordinary case for
a create — yet it was reported at error level, through `logAxiosError`, which
rendered the sandbox's own `cat: …: No such file or directory` as "An error
occurred while setting up the request" plus a stack. Every ordinary file
creation emitted an error-level entry claiming a transport fault about a file
that was written half a second later, and an operator reading it concluded the
file had been lost.

Split the two failures `readSandboxFile` was conflating. A request that never
completed still goes to `logAxiosError`; a request the sandbox answered with
stderr is now classified on that stderr — an absent path logs at debug, and
anything else (permission, a broken interpreter) keeps error level with the
sandbox's own wording. The thrown error is unchanged, so the authoring flow's
create/overwrite decision still reads it the same way.

The classifier moves to `packages/api/src/files/code/errors.ts` and is now
shared: `handlers.ts` had two near-duplicate copies of it, the narrower of
which documented itself as narrower than the broader one without either
deriving from the other.

Also carry the cause on `[preflightCodeOutputBatch] … could not be inspected`.
That warning follows each `create_file` in the same request, and the batch
discarded the error entirely — so a Code API that refused the download, one
routed to the wrong execution profile, and a file too large to inspect all
arrived as the same sentence, and neither this warning nor the one above could
be told apart from the failures around them. It goes through
`getSafeErrorMetadata`, which is what keeps the upstream message and the
artifact's name — both of which can echo submitted content — out of the log.
2026-09-04 09:21:24 -04:00
Dustin Healy
ab0d74b492
✏️ feat: Add Agent Management Update Endpoint (#15566)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
* feat: add Agent Management update endpoint

* fix: reject unsupported Agent update values

* test: simplify Agent update mocks
2026-09-04 00:16:57 -04:00
Dustin Healy
2ffa9ec420
➕ feat: Add Agent Management Creation Endpoint (#15564)
* feat: add Agent Management creation endpoint

* 🧫 fix: Reject Null Agent Management Models (#15581)

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-09-03 23:45:04 -04:00
Danny Avila
8a46ca070c
🧊 feat: Persist Context Fading Tier Across Agent Runs (#15505)
* 🧊 feat: Persist Context Fading Tier Across Agent Runs

Carry the pruner's latched context-fading tier from `@librechat/agents`
through `contextMeta` the way `calibrationRatio` already travels, so a
historical tool result keeps the same truncated bytes from one run to the
next and Anthropic's prefix-based prompt cache survives across turns
(danny-avila/agents#497).

- Add `IAgentFadingTier` to the message and conversation `contextMeta`
  types, schemas and the data-provider zod schema
- Add `isAgentFadingTier`, `resolvePersistableFadingTier` and
  `resolveRunContextMeta` in `packages/api`; a tier is persisted once it
  carries information (masking active or a budget below the window), and
  a latched tier is kept even at a neutral calibration ratio
- Seed `createRun({ fadingTier })` from the parent response's contextMeta
  regardless of encoding, since caps are character-based; the field rides
  the existing forward-compatible `runConfig` variable and older SDK
  versions ignore it, as does `Run.getFadingTier` being absent
- Accept a valid `fading` field in event actor context and reject a
  malformed one, matching the calibration checks
- Capture run context meta through one module-level helper for both
  completion paths instead of two inline copies

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Seed Fading Tier Across HITL Resume, Validate at the DB Layer

Address review findings on fading tier persistence:

- Leave the "is this tier worth persisting" decision to the SDK, whose
  `Run.getFadingTier()` now returns only informative tiers; comparing
  against the client's window misclassified untouched conversations
  whenever a reserve ratio was configured
- Carry `contextMeta` through the HITL pause projection (staged approval,
  job metadata, resume state) and seed the rebuilt client from it, so a
  paused turn resumes with the same tier and calibration instead of
  re-deriving a shallower one and rewriting the prefix
- Share run seeding between `chatCompletion` and `resumeCompletion`, and
  make capture tolerate partial runs and bare resume contexts
- Validate `contextMeta.fading` in `commitAgentEventActorState` with a
  shared `isAgentFadingTier` guard in data-schemas, so a malformed tier is
  rejected at the DB layer instead of silently cold-starting the actor
- Reuse `IAgentEventActorContextMeta` for `IMessage.contextMeta` instead of
  extending an inline duplicate

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 test: Cover Resume Seeding From Paused Context Meta

Assert the resume controller seeds the rebuilt client from the context meta
captured at the pause before rebuilding the run, and move the capture
helper's JSDoc back above its function.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧹 style: Sort Imports in Fading Tier Plumbing

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Persist Fading Tier Through Redis Jobs and Branches

Address Codex and Copilot review findings on fading tier persistence:

- Deserialize `contextMeta` in `RedisJobStore`, whose explicit read mapper
  rebuilt every other pause field but this one, so Redis-backed
  deployments resumed with no tier to seed after every HITL pause
- Copy server-private `contextMeta` onto the assistant message created by
  `POST /api/messages/branch`, so the next turn seeds its pruner the same
  way it would from the parallel source response
- Constrain the `fading` subdocuments in both Mongoose schemas (version
  enum, positive budget, required fields) to match the zod schema
- Throw a distinct "fading tier is invalid" error from both context meta
  validators instead of reporting a calibration failure

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Carry Context Meta Through Stop and Re-Pause

Addresses the second Codex pass on the fading-tier persistence:

- A re-pause whose resumed segment has nothing persistable now clears the
  job's `contextMeta` in the same transition instead of leaving the first
  pause's calibration and tier for the next resume to seed from.
- The client publishes the run's live calibration and fading tier onto the
  job after each pre-invoke context snapshot (deduplicated on value), and
  the Stop path copies `jobData.contextMeta` onto the stopped response, so
  a follow-up from a stopped turn keeps the prompt prefix stable.
- Mid-run captures (HITL pause, Stop) read the graph's live state; `Run`
  refreshes its own getters only after the stream settles, so the pause
  used to persist the seeds it started from.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Fence Context Meta Publication Ahead of Stop

A Stop handled by another replica reads the job before the owner is
signaled, so a fire-and-forget publish could lose the race, and a run
stopped before its first context snapshot never published the tier it
inherited. The client now publishes the inherited seed before the run
streams or resumes, and the context-usage handler awaits each
snapshot's publish so the write lands before the model call it
describes begins. Failures still only log; the pre-run publish is
optional-called so bare resume contexts keep working.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Read Back Context Meta After the Abort Claim

The run owner publishes its context meta ahead of each model call and
awaits the write, so a publish landing between the abort's initial job
read and its terminal claim describes the call whose partial output the
abort snapshot carries. abortJob now reads the same-epoch job back after
the claim and content re-read and takes its context meta, so the stopped
response persists the tier that produced its bytes. The refresh is
best-effort and logs on failure like the content refresh beside it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Share In-Flight Context Meta Writes and Clear Neutral Live State

Two snapshot callbacks in a parallel-agent run can capture the same tier
before the first write settles; the publisher now caches the in-flight
promise per serialized value so the second caller awaits the same
durable write instead of treating an uncommitted value as published.
A live snapshot with nothing left to persist after an earlier publish now
writes a neutral record (ratio 1, no tier), since a running job's fields
cannot be deleted through the metadata writer, so a Stop no longer
persists an earlier non-neutral state onto the response. The pre-run
seed publish is unchanged and is told apart from live snapshots by a
flag from the context-usage sink.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 chore: Drop Unused Rest Parameter in Context Meta Test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 feat: Persist Per-Agent Fading Tiers as Compact Context Meta

Reconciles the host with @librechat/agents at 9aacad1a, where graph
history stays canonical and each Run derives a provider-only projection
from the latched tiers. LibreChat therefore persists only compact state:
the calibration ratio, the default agent's tier, and now the per-agent
tiers from Run.getFadingTiers(), stored as validated entries so agent IDs
never become MongoDB field names and restored onto a null-prototype
record for RunConfig.fadingTiers. Truncated messages, canonical tool
content, projection provenance and per-message truncation state are
never persisted; the SDK's latched flag is stripped on capture.

The per-agent map travels everywhere the single tier already did:
response contextMeta, event-actor checkpoint state, HITL pause and
resume job metadata in both stores, the Stop path, and branch creation.
A shared Mongoose definition keeps the message and conversation schemas
identical, the zod schema validates entries, and commitAgentEventActorState
rejects malformed maps. Comments no longer describe graph messages as
truncated; only the provider projection is.

Tests cover: tool results and inputs reach the persisted content parts
at full size; contextMeta carries exactly the compact tier and
calibration fields; a persisted snapshot round-trips into the seeded
RunConfig unchanged and prototype-safe; per-agent tiers survive pause,
Redis job round-trips, and event-actor context validation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Restore Event Actor Types and Publish Seeds Before Abortable Setup

The previous commit's doc rewording in the conversation types dropped the
event-actor interfaces that precede the fading tier, which broke the
declaration bundle in CI; they are restored unchanged.

The inherited context meta is now published onto the job as soon as the
parent's state is loaded and awaited at the top of chatCompletion and
resumeCompletion, ahead of run creation and the other abortable setup
stages, so a Stop during setup still persists the parent's tiers onto the
stopped response. The partial response saved when every subscriber
disconnects now carries the job's context meta like the Stop and pause
paths do. The branch route keeps the source's context meta in the saved
message but strips it from the HTTP response, matching client reads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Serialize Distinct Context Meta Publications

Overlapping snapshots from parallel agents could carry different tiers;
each started its own write and both job stores keep whichever write
finished last, so an older snapshot could overwrite a newer one. Each
distinct publication is now issued after the previous one settles, in
call order, so the newest snapshot is what a Stop reads back. Equal
values still share one write.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Project Root Message Reads and Keep Inherited Meta on Setup Failure

The root GET /api/messages paths returned raw documents: the
single-message query read called getMessages without a projection and the
cursor page performed an unprojected find. Both now apply
CLIENT_MESSAGE_SELECT, with getMessagesByCursor accepting a select option,
so the persisted fading tiers never reach the client through the
paginated endpoint; the search-hydration read projects the same way.

A turn that loads a parent's context meta but fails before its run exists
used to replace the inherited meta with the capture of a missing run, so
the persisted error response lost the last valid tiers. Both finalizers
now keep the inherited meta when no run was created while a created run's
neutral state may still clear it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 refactor: Move Context Meta Publication Into packages/api

The publication coordinator (ordering, deduplication, in-flight sharing,
failure state) now lives in packages/api as createContextMetaPublisher,
with selectRunContextMetaToPublish deciding what a publication carries;
the legacy client keeps only the wiring to the job metadata writer. A
transiently failing write is retried with a short backoff before the
publication is reported and forgotten, so a snapshot's model call no
longer proceeds on the first rejection while the job holds the previous
record. The SDK swallows handler errors, so retrying is the only way to
strengthen the fence without failing the turn.

A terminal save now supplies contextMeta: null when the finished run has
nothing to carry, and saveMessage unsets a previously stored value in
that case, so a disconnect snapshot's record cannot outlive the run that
completed neutrally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Widen the saveMessage Method Type for the Null Unset

The exported method interface still typed contextMeta as the message
field, so the spec exercising the null unset failed typechecking; it now
mirrors the implementation signature.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Publish Every Agent's Snapshot, Sanitize Search Hits, Unbound Agent IDs

The context-usage handler now awaits the run's context meta publish for
every ON_CONTEXT_USAGE event, hidden sequential agents included, while
the visible-snapshot bookkeeping and the client emission keep their gate;
a Stop before the next visible snapshot finds the tier a hidden agent's
model call latched. Search results are built from the hydrated hit minus
its server-private contextMeta, since Meilisearch hydration projects every
schema field. The per-agent tier entry guard no longer caps the agent ID
length: ephemeral agent IDs encode endpoint, model and sender without a
bound, and dropping such an entry would let that agent alone re-derive
its tier on the next turn. Also formats the publication spec so the
static checks pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Unset Context Meta Atomically, Keep Publication State After a Failed Write

- `saveMessage` folds the `contextMeta` unset into the same update that
  persists the terminal response, on both the plain and the provenance-merge
  paths, so a failure between two writes can no longer leave a completed row
  carrying a disconnect snapshot's state.
- `createContextMetaPublisher` tracks whether any record has committed
  separately from the retryable latest publication, so `hasPublished` stays
  true after a later publication exhausts its retries and a following neutral
  snapshot still overwrites the earlier record on the job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Carry Context Meta Through the Resumable Stop Route and Neutral Resumed Completions

- The resumable Stop route copies the refreshed `jobData.contextMeta` onto the
  terminal assistant message it persists, as the abort middleware already
  does, so a stopped resumable response keeps the tiers that produced its
  partial output.
- A resumed run that completes with neutral context state now saves
  `contextMeta: null`, which unsets the paused segment's record instead of
  letting an omitted field keep it on the completed row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Trust Only Server-Authored Context Meta, Unset It on a Neutral Stop, Read It From the Job on Disconnect

- `POST /api/messages/:conversationId` and conversation imports drop any
  client-supplied `contextMeta`, and the run seeds its calibration and fading
  tiers only from a server-authored parent response, so a forged tier can never
  shape a provider projection.
- Both Stop paths save `contextMeta: jobData.contextMeta ?? null`, so a job that
  re-paused with neutral state unsets what an earlier pause stored on the row
  instead of leaving it behind.
- The disconnect partial save reads the same-epoch job record for the run's
  published context meta, since the client-facing resume snapshot never carries
  server-private state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧪 test: Mock the Job Store in the Resume Metadata Spec

The disconnect partial save now reads the same-epoch job record through
`GenerationJobManager.getJobStore()`, so the resume metadata spec's manager
mock provides it as the other disconnect spec already does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 🧊 fix: Keep Context Meta Off the POST Message Response

`POST /api/messages/:conversationId` returned the saved row verbatim, so a
client write against an existing server-authored message echoed the row's
stored context meta. The route now projects the row through the same client
projection the branch route uses, which drops the server-private field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

* 📦 chore: Bump @librechat/agents to 3.7.16 for Persisted Fading Tiers

3.7.16 is the first release that carries danny-avila/agents#497, so
`Run.getFadingTier()` / `getFadingTiers()` now report the latched tier
this branch persists and `RunConfig.fadingTier` / `fadingTiers` seed the
next run from it. The dependency set is unchanged from 3.7.15; only the
package entry moves.

Adds an SDK-backed round trip to the fading spec: a first pruner latches
an informative tier, `resolveRunContextMeta` reduces it to the persisted
shape, and a second pruner seeded from that shape reproduces the first
run's projected tool-exchange bytes under instruction and calibration
drift, while an unseeded pruner under the same drift relaxes its tier and
rewrites them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNbgMeR2aZDyhsqAy4L8gF

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-02 20:29:43 -04:00
Danny Avila
29f738dea8
⏭️ feat: Honor an Interrupt Before the Model Has Answered (#15491)
* ⏭️ feat: Honor an Interrupt Before the Model Has Answered

An interrupt armed while the model is still thinking sat as "Interrupting"
until the entire turn finished, then landed as a terminal continuation. The
cause is upstream: the SDK reads `shouldPreempt` once per streamed chunk, and
its seal gate requires non-empty text — so a silent provider is never polled at
all, and a reasoning-only turn is never sealable.

`@librechat/agents` gains a discard-and-restart path for exactly that window
(`HOOK_PREEMPT_RESTART_CAPABLE`). This wires the host half:

- `GenerationJobManager.subscribePreempt` registers run-scoped wake listeners,
  notified after an ACCEPTED arm — local or cross-replica, since both land in
  `armPreemptIds`. Fenced on `createdAt` like every other preempt entry point,
  and a throwing listener never fails the arm a queued steer depends on.
- `createSteerPreemptPoll` hands that channel to the SDK as
  `StreamPreemption.subscribe`, gated on a new `isSteerPreemptRestartSupported`
  probe so an SDK that cannot act on a wake is never handed one.

The wake is a hint only: `isPreemptRequested` stays the single authority, so
the level-triggered contract the seal path depends on is unchanged.

Inert until `@librechat/agents` publishes the restart contract — the probe
returns false against the pinned 3.7.11 and the run keeps today's behavior.

* 🩹 fix: Replay an Already-Armed Preempt on Subscribe

Preempt requests are level-triggered, and the window between a job becoming
steerable and the SDK installing its model-attempt listener is real. An
interrupt landing there was recorded with no callbacks to notify, and on a
silent or reasoning-only turn no later chunk poll may ever run — leaving the
request to wait out the whole turn, the exact stall this channel removes.

`subscribePreempt` now replays an existing arm to the newly registered listener
only, after registration: waking the whole set would re-notify runs that
already looked, and waking before registration would leave a concurrent arm
with nowhere to land. The `createdAt` fence is unchanged and still gates the
replay.

The SDK also reads the level flag once at each attempt's start, so this is
belt-and-braces rather than the sole cover — but it makes the host correct on
its own terms instead of resting on when the SDK happens to look.

* 🔧 chore: Update @librechat/agents dependency to version 3.7.14 in package-lock.json and related package.json files

* 🔧 chore: Update dependencies in package-lock.json and package.json

- Bump versions for @humanfs/core (0.19.1 to 0.19.2), @humanfs/node (0.16.6 to 0.16.8), @xmldom/xmldom (0.8.13 to 0.8.15), and qs (6.15.2 to 6.16.0).
- Add @humanfs/types (0.15.0) as a new dependency for @humanfs/node.
- Update dependencies for side-channel and side-channel-list to their latest versions.
2026-09-02 13:35:52 -04:00
Dustin Healy
0b96fb9f70
🪪 feat: Propagate Tenant to Agent Model Headers (#15521)
* fix: propagate tenant to agent model headers

* fix: propagate tenant across agent model calls
2026-09-02 13:25:38 -04:00
Danny Avila
1c7a172321
🕹️ feat: Configure BYOM Tool Permissions (#15504)
* feat: add configurable BYOM permissions

* test: cover BYOM permission settings live

* fix: harden BYOM permission settings

* perf: reuse principals for environment discovery

* chore: normalize BYOM settings style

* fix: Align BYOM Discovery and Admission

* fix: Harden BYOM Permission Boundaries

* fix: Align BYOM Skill Approval Routing

* fix: Preserve Lazy BYOM Approval Capabilities

* perf: Parallelize Cached Environment Reads
2026-09-02 10:38:00 -04:00
Danny Avila
b1c56239f9
🧤 feat: Enforce Safe BYOM Tool Approvals (#15503)
* feat: add safe default approvals for BYOM code

* fix: preserve safe BYOM approval baseline
2026-09-02 08:53:16 -04:00
Marco Beretta
2cf1007f3c
🧺 feat: Preserve Partial Work at Tool Call Limits (#15486)
* feat: Handle reaching the agent tool-call limit gracefully

When the graph exhausts its per-turn step budget, persist the turn as
unfinished with finish_reason tool_call_limit instead of a red error.
Warn the model as the budget runs low, and offer Keep going or Answer now.

* docs: Add screenshots of the tool-call-limit notice

Light and dark captures of the unfinished-turn card for the PR description.

* style: Tighten the tool-call-limit notice card

Pin dismiss to the top-right with uniform rounding, use a slightly
smaller hit target, and round the card to xl. Refresh the PR screenshots.

* fix: Address Copilot review findings on the step-budget path

Skip empty and subagent-scoped PostToolBatch events when counting
rounds. Drop the double-cast in the notice test helper and coerce the
tool-call-limit finish reason in the Continue-gate spec.

* fix: Address Codex review findings on budget and dismiss

Pass the resolved recursion limit into the OpenAI-compatible
processStream path so the step-budget hook matches the graph.
Reset the notice dismiss state when the displayed message id changes.

* fix: Unblock CI typecheck and import sorting

Declare recursion_limit on the OpenAI service agent type, sort that
file's local imports, and route the notice test context through unknown
so tsc accepts the partial ChatContext fixture.

* fix: Sort remaining PR imports for static checks

* chore: Remove tool-call-limit notice screenshots

* fix: Keep recovery actions from consuming composer drafts

Pass empty file, skill, and quote overrides on Keep going and Answer
now so a staged compose is not attached or drained.
2026-09-01 21:38:02 -04:00
Danny Avila
164f146bdf
🛞 feat: Manage Personal Code Environment Lifecycles (#15369)
* feat: add self-service code worker pairing

* fix: enforce code worker principal boundaries

* fix: separate pairing control planes from execution

* fix: preserve principal worker routing

* fix: pin artifact traffic to code workers

* fix: authenticate code worker selection

* test: mock worker-aware artifact headers

* fix: harden self-service code worker routing

* fix: fail closed for principal code pairing

* feat: manage personal code environments

* fix: harden personal code environment lifecycle

* fix: satisfy backend lifecycle types

* fix: harden principal environment lifecycle and quotas

* fix: fence environment lifecycle races

* fix: reconcile code environment stack

* style: format code execution headers

* fix: reconcile code environment lifecycle

* fix: close self-service pairing gaps

* fix: close code environment reconciliation gaps

* fix: close code environment UI review findings

* fix: satisfy code environment type checks

* fix: route code priming to selected worker

* fix: harden code environment lifecycle fences

* fix: type lease renewal interval

* fix: harden environment lifecycle recovery

* fix: close lifecycle recovery races

* fix: type compensated agent timestamps

* fix: fence lifecycle cleanup by tenant and revision

* fix: scope environment revision fencing
2026-09-01 21:10:05 -04:00
Danny Avila
a3c2277c38
🚧 fix: Narrow Chat MCP Selections to What the Picker May Offer (#15478)
* 🚧 fix: Narrow Chat MCP Selections to What the Picker May Offer

The chat MCP picker offers a server only when the operator has not hidden it
(`chatMenu: false`) and the user reaches it directly rather than through an
agent (`consumeOnly`). Nothing enforced either condition on the request, so a
stale client, a replayed body, or a hand-written one could name any server and
get its tools — `consumeOnly` in particular is a permission the registry derives
per user, so ignoring it let a user reach in plain chat a server they had only
been granted through an agent.

Narrow the picker's selection in the two ephemeral loaders, against the same
predicate the client's `selectableServers` uses. The resolver is injected, so
`packages/api` stays free of the registry and the loaders keep their existing
`deps` shape.

Provenance is why this lives in the loaders rather than at the tool-resolution
chokepoint: only here is a server name known to have come from the picker.
Servers a model spec pins are the operator's own choice and are added after the
narrowing, so pinning a chat-hidden server to a spec keeps working.

`chatMenu` only ever comes from the config tier, so the overlay settles it with
no lookup at all. Lookups for the rest run concurrently and are cached per
server and user. A name the registry cannot resolve — request-tier, declared on
the body and never registered — is kept, as is one whose lookup fails: this
narrows an already-authenticated selection and is not the authorization
boundary.

* 📝 docs: Say What chatMenu Now Bounds

The schema still described `chatMenu` as dropdown visibility alone, which is
misleading now that it also bars a server from the chat selection a request
carries. Spell out both halves, and what it deliberately does not restrict:
agents, and servers a model spec pins.

* 🛡️ fix: Close Four Gaps in the Chat MCP Narrowing

Dedupe before resolving. The selection is request-supplied and the loaders used
to build a `Set` immediately; mapping the raw array first let one body repeat a
name enough times to launch a resolver — registry, cache, and ACL reads, with no
single-flight — per occurrence.

Resolve names through the tool-loading aliases. A configured `private server` is
addressable as `private_server` downstream, so an exact-key lookup let the
normalized spelling walk past a `chatMenu: false` check and still load tools.
Names are returned as sent; only the decision is canonicalized.

Let the request overlay settle `chatMenu`. The config tier is where the flag
lives and is what the picker itself was built from, so an overlay that re-enables
a server now outranks the durable config the registry returns, which was silently
dropping a valid selection. `consumeOnly` still comes from the registry.

Narrow the request body, not just the loader's copy. `applyContextToAgent` reads
`req.body.ephemeralAgent.mcp` directly and prefers it over the agent's tools, so
a hidden server's `serverInstructions` reached the model context even after its
tools were dropped.

* 🧭 fix: Ask the Registry Which MCP Servers the Picker May Offer

Three rounds of review found the same mistake in three disguises: this narrowing
re-derived the registry's tier precedence instead of asking for it. A config
entry was treated as authoritative over a user-DB or process-backed server of the
same name; the alias map was built from config names only, so a DB server named
`private_server` was misrouted to a config `private server`; and the overlay
short-circuit ran before any of that could be consulted.

Resolve the accessible set through `resolveAllMcpConfigs` instead — the same call
behind `GET /api/mcp/servers`, so what the request may use and what the picker
was offered come from one place and cannot drift. Precedence, cross-tier name
collisions, and the config overlay are the registry's job again, and the
selectability test is a single flag check over what it returns.

The result is smaller: no overlay branch, no per-server lookups, one catalog read
regardless of how long the selection is, and aliases built over the whole
accessible set so an exact name always outranks another server's normalized form.

Publish the servers actually in play back onto the request body, spec pins
included. `applyContextToAgent` prefers that list over the agent's tools, so
narrowing to the picker selection alone had started dropping a spec-pinned
server's `serverInstructions` while suppressing a hidden one's.
2026-09-01 18:47:36 -04:00
Danny Avila
51edb2a660
🛫 feat: Enable Self-Service Code Worker Pairing (#15367)
* feat: add self-service code worker pairing

* fix: enforce code worker principal boundaries

* fix: separate pairing control planes from execution

* fix: preserve principal worker routing

* fix: pin artifact traffic to code workers

* fix: authenticate code worker selection

* test: mock worker-aware artifact headers

* fix: harden self-service code worker routing

* fix: fail closed for principal code pairing

* style: format code execution headers

* fix: close self-service pairing gaps

* fix: satisfy code environment type checks
2026-09-01 18:46:33 -04:00
Danny Avila
aa7539909b
🪶 feat: Enable Standalone Skill Authoring (#15480)
* feat: enable standalone skill authoring

* test: align skill e2e flow with enable toggle

* fix: preserve legacy skill state on version restore
2026-09-01 18:39:05 -04:00
Danny Avila
dccd7ae496
🧶 fix: Preserve Multi-Agent Tool Call History (#15468)
* 🧭 fix: Preserve Multi-Agent Tool History

* 🧰 fix: Preserve Lazy Subagent Tool History

* 🧪 test: Enable Lazy Code History Fixture

* 🧭 fix: Derive Effective Lazy Tool History

* 🧪 fix: Tolerate Empty MCP Name Audits

* 🧪 test: Mock Lazy MCP Name Audit

* 🧭 fix: Expand Lazy Historical Tool Names

* 🧱 fix: Derive Durable Wildcard History

* 🧬 fix: Traverse Nested Wildcard History

* 🔭 fix: Resolve Wildcard Server Boundaries

* 🔐 fix: Persist MCP Call Identity

* 🪪 fix: Stamp Resolved MCP Execution Identity

* 🕸️ fix: Resolve Graph Member MCP Identity

* 🧭 fix: Fall Back From Empty Graph Member ID

* 🧭 fix: Preserve Aggregated MCP Identity

* 🧭 fix: Harden MCP Identity Resolution

* 🧭 test: Assert Concurrent Graph Limit Invariant

* 🧭 fix: Centralize Lazy History Policy

* 📦 chore: Bump `@librechat/agents` to v3.7.13

* 🧪 test: Align Content Filter Fixture With SDK History
2026-09-01 18:37:45 -04:00
Danny Avila
97e1e950ad
🧲 fix: Draw OpenID Refresh Lookups Into System Context (#15451)
* fix: scope refresh user lookups to system context

* fix: preserve refresh system context

* fix: bind refresh recovery to user tenant
2026-09-01 02:02:21 -04:00
Danny Avila
49e1e32f3a
🧺 fix: Funnel MCP Catalog Work Through a Shared Gate (#15429)
* 🧯 fix: Bound MCP Catalog Work Across Requests

* 🛑 fix: Reject Saturated MCP Discovery

* fix: fairly bound MCP catalog work

* test: type catalog snapshot mock arguments

* test: assert snapshot deadline forwarding

* fix: harden MCP catalog scheduling
2026-08-31 14:13:31 -04:00
Danny Avila
982a9a5141
🧴 fix: Strip LangChain URLs From Provider Errors (#15421)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* 🧹 fix: Strip LangChain Attribution From User-Facing Provider Errors

LangChain classifies provider failures by mutating `error.message` with a
docs URL, and that message is forwarded verbatim into the error content part
we persist, so users saw `docs.langchain.com/...` credited for a provider's
own 429. The client only intercepted `MODEL_NOT_FOUND`, leaving every other
code to render raw.

Resolve the classification server-side instead: codes with localized copy
become the typed payload the client already knows how to localize, and every
other message keeps the provider's wording with the docs URL removed before
it reaches the database. The untouched error still reaches the logs through
`getSafeErrorMetadata`.

The client keeps a code-driven lookup for messages persisted before this, and
gains `com_error_model_rate_limit` alongside the existing not-found copy. The
OpenAI-compatible and Responses controllers now share one provider-error
helper rather than three copies of it, so the strip applies on those surfaces
too.

* 🛡️ fix: Harden Error Sanitization For Non-String Messages And Legacy Text

Codex review follow-up.

`stripLangChainTroubleshootingUrl` assumed a string, so an error-like object
with a truthy non-string `message` threw `.replace is not a function` inside
the error handler that was meant to report it — the template interpolation it
replaced had coerced such values safely. It now coerces at the boundary, as
does the code lookup.

Legacy messages whose LangChain code has no localized copy still rendered the
docs URL, which is exactly the historical case the client-side path exists to
clean up. The rendered fallback is now stripped too.

Both helpers move to `librechat-data-provider`, which is shared by the client
and the backend, so one implementation serves both instead of a second regex
drifting in the component.
2026-08-31 12:51:34 -04:00
Danny Avila
b7ae67307e
🕰️ fix: Anchor Temporal Variables to Turn Start (#15419) 2026-08-31 10:38:10 -04:00
Danny Avila
413906abc3
🖍️ fix: Honor Agent Image Detail Settings (#15407)
* 🖼️ fix: Apply an Agent's Configured Image Detail

The Agent Builder exposes an Image Detail slider and saves it to
`model_parameters.imageDetail`, but nothing ever read it back: `encodeAndFormat`
resolves the level from `req.body.imageDetail`, which the agents route never
populates, so every image was encoded as `detail: "auto"`. Providers that reject
`auto` — MiniMax's OpenAI-compatible API among them — fail every image upload
through an agent, and the setting that would fix it has no effect.

The root cause is a classification error rather than a missing argument.
`extractLibreChatParams` splits LibreChat's own settings out of an agent's model
parameters, and its key set is derived from the `librechat` group in
`parameterSettings`. `imageDetail` sat in `baseDefinitions` instead, so it was
neither extracted for LibreChat's use nor stripped from the options handed to the
provider.

It is now grouped with `resendFiles` and the other LibreChat-level parameters,
which gives it the same path those already take: extracted during agent
initialization, carried on the initialized agent, passed to the client, and read
by `encodeAndFormat`. `AgentClient.getSaveOptions` already expected
`options.imageDetail` — nothing had ever populated it.

The request body still wins where it is set, so the classic endpoints keep
resolving detail from conversation settings exactly as before.

Fixes #15359

* 🔤 fix: Type Image Detail as Its Enum, Not a Bare String

`TConversation.imageDetail` is the `ImageDetail` enum, so the extractor's return
type and the initialized agent's field are now declared as that enum rather than
widened to `string`, and the tests pass enum members instead of raw literals.

Caught by CI's type check, which I had run before adding the tests and not after —
jest compiles the specs through babel, so the suite stayed green over a type error.
2026-08-31 08:34:05 -04:00
Danny Avila
c7f0a8f247
📬 feat: Move Agent Queued Turns Server-Side (#15370)
* feat: move agent queued turns server-side

* test: update queued turn composition coverage

* fix: harden queued-turn lifecycle ownership

* fix: centralize queued-turn lifecycle ownership

* fix: close queued-turn lifecycle gaps

* fix: unify queued turn lifecycle transitions

* fix: fence queued-turn deletion admission

* fix: preserve queued-turn lifecycle ownership

* fix: close queued-turn reconciliation races

* fix: reconcile quarantined queued-turn admissions

* fix: annotate admission evidence default

* fix: deepen queued turn lifecycle admission

* fix: fence queued turn provider admission
2026-08-30 21:53:58 -04:00
Danny Avila
fcae1025c0
🧳 feat: Register Principal-Owned Code Environments (#15365)
* feat: add principal-owned code environments

* fix: address code environment CI coverage

* fix: expose principal code environments in endpoint config

* fix: reject missing code environment bodies

* fix: harden code environment lifecycle

* fix: revalidate principal code environments

* fix: synchronize code environment authorization

* fix: bind code environments to current principals

* fix: fail closed on code ACL cache errors

* fix: prevent code environment override shadowing

* fix: preserve principal environment defaults

* fix: suppress revoked code environment aliases

* fix: fail closed on environment augmentation

* fix: narrow code environment defaults

* fix: isolate code environment fallback

* style: sort code config imports
2026-08-30 19:33:19 -04:00
Danny Avila
7533d138fa
🧬 perf: Evolve Compaction Guidance on Warm Turns (#15371)
* perf: evolve compaction guidance on warm turns

* style: sort compaction adapter imports
2026-08-30 17:11:20 -04:00
Danny Avila
30124f21b2
🎻 refactor: Orchestrate Agent Runs Through a Request-Free Host (#15366)
* refactor: decouple agent initialization from HTTP

* refactor: centralize remote agent execution lifecycle

* fix: preserve pre-settlement error rendering

* fix: preserve request-backed tool loading

* style: sort agent execution imports

* fix: adapt public agent tool loaders
2026-08-30 15:38:57 -04:00
Danny Avila
c1cb591d49
🗿 feat: Add Attached Stateful Code Environments (#15352)
* feat: add attached stateful code environments

* fix: include code environment in lazy agent type

* fix: harden stateful environment routing

* fix: complete code environment route isolation

* fix: declare code environment map type

* fix: preserve configured code execution routes

* fix: harden stateful environment updates

* test: assert route-scoped sandbox readiness

* fix: harden stateful environment lifecycle

* fix: isolate migrated code sessions

* test: assert route-qualified code sessions
2026-08-30 15:38:45 -04:00
Danny Avila
cd3768ed1f
🍵 feat: Continue Late Steers in Warm Agent Runs (#15357)
* feat: continue late steers in warm agent runs

* chore: bump agents sdk to v3.7.9

* fix: guard terminal steer admission
2026-08-30 11:54:17 -04:00
Danny Avila
2ae6c8aea9
🛎️ feat: Wake Agents on Background Tool Completion (#15350)
* feat: wake agents for background tool completion

* fix: isolate background completion contracts

* fix: anchor background completion identity

* fix: close background completion delivery gaps

* fix: preserve manual background polling

* fix: preserve legacy tool group identity

* fix: close background wakeup identity gaps

* test: type background wakeup enqueue mock

* fix: preserve background completion identity

* test: type activity phase fixture

* test: mock phase media query

* fix: arbitrate background result ownership

* fix: retain tool step routing helper

* test: expand phase groups before identity checks

* fix: preserve background completion ownership

* fix: bound durable background results

* fix: type wakeup input budget export

* chore: sort background handler imports

* fix: persist timed-out background completions

* fix: reconcile background completion ownership

* fix: type background completion capabilities

* fix: harden background completion terminalization

* test: type missing completion evidence

* fix: require evidence for completion retirement

* chore: satisfy background completion static checks

* test: cover automatic background completion wakeups

* test: assert background wakeup agent identity

* fix: wake capability-fenced trigger deliveries

* test: keep capability shield fixture public

* test: assert capability worker wakeup

* fix: close background completion ownership gaps

* test: satisfy completion lease static checks

* fix: preserve artifact completion wakeups

* fix: close background delivery recovery gaps

* fix: simplify background receipt guidance

* fix: recover dead background completion batches

* fix: fence background completion recovery

* fix: recheck background recovery ownership

* fix: fence unpublished background continuations

* test: await background message hydration
2026-08-30 08:56:41 -04:00
Danny Avila
e3ccaba5af
🛄 feat: Restore Compaction Guidance Across Continuations (#15356)
* 🧭 feat: preserve compaction guidance across continuations

* 🔧 fix: narrow persisted compaction fields
2026-08-30 08:13:24 -04:00
Danny Avila
70f735336d
🛎️ fix: Enroll Remote Agent Runs in the Generation Lifecycle (#15349)
* fix: enroll remote agent runs in generation lifecycle

* fix: close remote lifecycle ownership gaps

* fix: close remote conversation drain races

* fix: fence remote runs during conversation deletion

* fix: reconcile remote deletion and settlement races

* fix: complete owner deletion recovery

* fix: preserve remote cleanup receipts

* fix: consume deletion receipts before cleanup

* fix: expose idempotent deletion option

* chore: sort remote lifecycle imports
2026-08-30 07:14:13 -04:00
Dustin Healy
8fcab7e44f
🔄 fix: Recover Missing MCP Marketplace Catalogs (#15323)
* fix: recover missing MCP marketplace catalogs

* fix: make MCP catalog recovery passive

* test: type MCP catalog recovery fixtures

* fix: bound and back off passive MCP catalog recovery

Passive recovery runs inline on `GET /api/mcp/tools` and its results are
request-local by design, so every list request re-dialed the same cold
servers with the default connection timeout. Three limits keep that cost
proportional to what recovery can actually recover:

- Cap the discovery timeout at 5s instead of inheriting the connection
  default (`initTimeout ?? 30s`); a server configured to connect faster
  keeps its own shorter limit.
- Skip a server the config tier already marked `inspectionFailed`, leaving
  it to that tier's retry window rather than re-dialing it per request.
- Skip a server whose declared `customUserVars` are unset, matching the
  gate `reinitMCPServer` applies for issue #10969 — connecting without them
  fails auth, so the attempt is spent for nothing.

Servers that still fail discovery enter a one-minute per-process cooldown,
which is what stops an unreachable server from being re-dialed by every
subsequent list request. A server that recovers clears its own entry, and
expired entries are swept at most once per window so the map stays bounded.

Skipped servers render exactly as they did before recovery existed: present
in the catalog with an empty tool list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xr1Dabvdn1mzzyYgpJgU5B

* fix: bound passive MCP recovery by deadline, key cooldowns by config

Both follow-ups address the same mistake: recovery expressed its own
request-level constraints in terms borrowed from other layers.

`connectionTimeout` bounds one connection attempt, and
`MCPConnectionFactory.discoverToolsInternal` spends it twice — once on the
authenticated connection, then again in `attemptUnauthenticatedToolListing`
— so capping it bounded no total this layer could reason about. Recovery now
enforces its own wall-clock deadline per server with `withTimeout`, which
holds however many attempts the factory makes; `connectionTimeout` is left to
do only its own job, still honouring a shorter operator `initTimeout`. An
attempt abandoned by the deadline disposes its own connection when it
settles, and `Promise.race` keeps a handler on it, so a late rejection is
not unhandled.

A per-request budget now caps total recovery regardless of server count.
A server is dialed only if the remaining budget can fund a full deadline;
never dialing one is not evidence against it, so a skipped server records no
cooldown and a later request reaches it once those ahead are cached or
cooling down.

Cooldown identity now includes the publication generation — the same
effective-config identity the tool caches fence on — instead of just user and
server name. Correcting a server's URL or transport keys a new entry, so the
refetch the client issues on update is no longer skipped for up to a minute
by the previous configuration's failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xr1Dabvdn1mzzyYgpJgU5B

* refactor: keep passive MCP recovery stateless and bounded by its own work

Reverts the cooldown, request budget and deadline race added in 257d5cf and
fc3e3c9, and keeps only the three stateless limits.

The tool cache refuses unfenced writes (`tools.ts`), and a discovery
connection owns no publication generation and is disposed, so a recovered
catalog cannot be retained by design. Those commits responded by building a
cache-shaped memory in front of it — per-process failure state, a scheduling
budget, an identity, an eviction sweep — and each round of review found
another way that hand-rolled cache differed from a real one: wrong identity
for configuration, wrong identity for credentials, no fairness across
requests, and a limiter slot released while its network operation was still
running. None of that machinery was asked for; all of it was compensation for
a result the architecture does not allow keeping.

Recovery is now stateless. It skips only what configuration alone proves
pointless — a server the config tier already marked `inspectionFailed`, and
one whose declared `customUserVars` are unset — and bounds the work itself
rather than racing it, so a limiter slot is held for exactly as long as its
network operation runs and the concurrency limit of three is real.

The attempt timeout is not a compromise: recovery exists for a server that is
reachable and authorized but whose catalog cache expired, and such a server
answers tools/list well inside 1.5s. Anything slower cannot be rescued here,
so failing fast costs nothing. The factory spends that value per attempt, so
a server's ceiling is it times the attempts made; the constant documents that
rather than hiding it behind a number tuned to today's attempt count.

Consequences that were bugs are now gone by construction: every cold server
is attempted on every request, so none is starved by those ahead of it, and
correcting a server's configuration or credentials takes effect on the next
refetch instead of waiting out a stale cooldown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xr1Dabvdn1mzzyYgpJgU5B

* fix: correct the inspection-failure skip and bound catalog fan-out

Three fixes that belong to this layer; a fourth issue does not, and is
described below.

The `inspectionFailed` skip was too broad. `MCPServersInitializer` stores a
YAML server that was unreachable at startup via `addServerStub`, which stamps
`source: 'yaml'`, and only config-tier entries get the timed retry in
`ensureSingleConfigServer`. Skipping every failed stub therefore hid a
recoverable server from the marketplace permanently — the exact state this
recovery exists to escape. It now defers only `source === 'config'`, matching
what `reinitMCPServer` already does.

Plugin auth is read only when some cold server actually declares
`customUserVars`, and only for those servers. The common unauthenticated case
no longer pays a MongoDB round trip whose result nothing can consume.

Snapshot refreshes are now bounded by the same limiter as discovery. They are
not local reads: both connection paths reach `fetchOrderedToolsSnapshot` and
issue a real `tools/list`, so a cache reset across many servers previously
burst unbounded outbound requests while discovery was capped at three.

Not fixed here, because it cannot be: `connectionTimeout` does not bound
discovery. It covers `connection.connect()` only, and `fetchToolsSnapshot`
then applies its own `TOOLS_LIST_TIMEOUT_MS` (30s) to `tools/list`, so a
server that connects fast and stalls while listing still holds its slot for
that window. The factory also does not cancel a timed-out connect before
starting the unauthenticated fallback. Bounding this end to end needs a
deadline threaded through `MCPConnectionFactory` into both `connect()` and
`fetchToolsSnapshot()`, which is a change to shared connection machinery
rather than to this caller. The constant's comment now states what it does
and does not bound instead of implying an end-to-end guarantee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xr1Dabvdn1mzzyYgpJgU5B

* fix: thread live-session OBO context into passive catalog discovery

The merge of #15334 sources OBO tokens from the live OpenID session via
request-boundary closures. Passive catalog recovery is a discovery call
site too; without these options an OBO server whose stored token went
stale fails recovery — the exact cold-catalog class this PR fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xr1Dabvdn1mzzyYgpJgU5B

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-30 06:55:30 -04:00
Danny Avila
fa913148fb
🔒 fix: Refresh MCP OBO Tokens From the Live OpenID Session (#15334)
* 🧊 fix: Inline-refresh OpenID session tokens at MCP OBO call time

Resolves the walk-away failure mode where MCP tool calls using OBO auth
fail with "No valid OpenID access token is available for OBO exchange"
after a user idles past their access-token lifetime. The strategy-time
snapshot on `user.federatedTokens` could expire mid-stream before
`resolveOboToken` ran, while `req.session.openidTokens` carried a still-
valid (or refreshable) token that nothing read.

- New OpenIDSessionRefresh service: per-user single-flighted closure that
  reads `req.session.openidTokens` at OBO time and inline-refreshes via
  `openid-client.refreshTokenGrant` when expired (30s skew), persisting
  via `req.session.save()`. No cookie writes (headers already flushed).
- `resolveOboToken` gains a required UpstreamTokenProvider parameter
  (typed as `() => Promise<OIDCTokens | null>`, reusing the shared shape
  from @librechat/data-schemas). Compile-time guarantee that every call
  site is updated.
- New `session_refresh_failed` OboTokenResolutionReason distinguishes
  "session expired and IdP rejected refresh" from "no upstream token
  ever existed."
- `req` threaded through createMCPTool/createMCPTools/createToolInstance
  to construct the closure with captured request, plus fail-closed
  guards in MCPConnectionFactory.getOboTokens and MCPManager.callTool
  when the closure isn't plumbed.
- Startup warning in MCPServersInitializer when OBO is configured but
  OPENID_REUSE_TOKENS is unset (the strategy populating
  user.federatedTokens is only registered under reuse, so OBO would
  fail every call without it).

Tests: 16 new in OpenIDSessionRefresh.spec.js; obo.spec.ts extended
for the new param + error reason; wiring smoke tests in MCPManager,
MCPConnectionFactory, MCPServersInitializer, and MCP.spec.js.

* 🛡️ fix: Harden OBO inline-refresh against token type and session edge cases

- Token-preference asymmetry: live-token reuse and expires_at derivation
  now strictly gate on the access_token, not the id_token. Added a
  required `tokenPreference` parameter on isLiveSessionTokenStillValid,
  buildOIDCTokensFromSession, and createOpenIDSessionTokenProvider
  so every call site is explicit. Dropped the bogus id_token-exp
  fallback in performIdpRefresh — id_token TTL is governed by IdP
  session policy and would mark a short-lived access_token reusable
  past its real lifetime.
- Missing req in /reinitialize route: the manual reconnect
  endpoint now forwards req into reinitMCPServer, so OBO servers can
  build a session-aware upstream-token closure instead of failing with
  missing_upstream_token.
- Single-flight key collisions: composed key as
  tenantId:openidIssuer:openidId:sessionId via getSingleFlightKey.
  Concurrent calls in the same session still coalesce; separate sessions
  never share an in-flight refresh, preventing refresh-token rotation
  from breaking sibling sessions and preventing cross-tenant token
  crossover when distinct users share an IdP sub.
- Opaque access token reuse): persist accessTokenExpiresAt
  (unix seconds, from tokenset.expires_in) on each refresh AND on initial
  login / SPA refresh in setOpenIDAuthTokens. New getAccessTokenExp
  helper falls back to it when the access token isn't a JWT, avoiding
  redundant inline refreshes for Microsoft Graph and Auth0 default
  audiences.
- Log hygiene: the single-flight key (containing sessionId,
  openidId, openidIssuer, tenantId) is now SHA-256-hashed in the
  "Joining in-flight refresh" debug log. Preserves cross-line correlation
  via a 12-char prefix without leaking credential or PII material.

Documented req.session.openidTokens shape contract via JSDoc typedef so
the new accessTokenExpiresAt field has a discoverable home alongside the
existing accessToken/idToken/refreshToken/expiresAt/lastRefreshedAt.

Tests: OpenIDSessionRefresh.spec.js up to 30 passing (added coverage for
opaque-token reuse, JWT-access-token-exp fallback, no-id_token-fallback
regression, cross-session no-coalesce, persistence on refresh, and a
guard against stale accessTokenExpiresAt carryover). AuthService.spec.js
adds two cases covering accessTokenExpiresAt persistence on login.
mcp.spec.js (route) gains a regression test asserting req flows into
reinitMCPServer.

* 🔍 fix: Detect OBO-only MCP admin config overrides

Admin Config overlays for YAML-defined MCP servers compare only
ADMIN_CONFIGURABLE_FIELDS to decide whether to lazy-init a config-tier override.
The OBO config field was added after that fingerprint list, so an override that
only added or changed `obo` was treated as unchanged YAML and skipped.

Include `obo` in the admin-configurable field list and add a regression test for
an OBO-only override.

* 🔊 fix: Mock MCP OAuth timeout in SDK integration test

MCPConnectionFactory.attemptToConnect reads mcpConfig.OAUTH_HANDLING_TIMEOUT
when building the OAuth connection timeout. The SDK OAuth integration test
mocked mcpConfig without that field, which made the timeout calculation produce
NaN and caused the test to fail before the OAuth refresh/start path completed.

Add OAUTH_HANDLING_TIMEOUT to the test mock.

* ♻️ refactor: Pass OBO upstream-token closure into MCP instead of req

Build the OpenID upstream-token provider at the request boundary and thread
only the closure through MCP handling, so the MCP service layer no longer
receives the raw Express request. The closure still reads/refreshes the live
session at tool-call time, preserving the walk-away recovery.

- Drop `req`/`capturedReq` from createMCPTools, createMCPTool, reconnectServer,
  createToolInstance, and reinitMCPServer; forward `upstreamTokenProvider`
  instead. Closure is constructed in loadTools, loadToolDefinitionsWrapper, and
  the reinitialize route, where req/res are in scope.
- OBO: fall back to user.federatedTokens when the provider yields no live
  session, so OIDC remote-agent calls (verified bearer, no session) still work.
- Inline refresh: mirror a rotated refresh token to the refreshToken cookie via
  a shared setRefreshTokenCookie helper, guarded by !res.headersSent (no-op on
  the streaming path; session copy stays authoritative).
- Single-flight: hydrate a joining request's own session from the resolved
  tokens so a later OBO call doesn't replay a rotated-away refresh token.

Addresses owner feedback and three review findings.

* 🔒 fix: Recover OIDC refresh-token rotation after SSE OBO refresh

When an inline OBO refresh rotates the OpenID refresh token after SSE headers
have already been sent, the browser refreshToken cookie cannot be updated. Store
a short-lived encrypted bridge from the stale cookie token to the rotated token
so /api/auth/refresh can recover after express-session loss.

Use the signed openid_user_id cookie to load user context for bridge validation,
retry only on invalid_grant, and delete the bridge only after the bridged refresh
succeeds.

* 🔨 fix: hydrate joined OIDC refresh sessions with stable refresh tokens

Update single-flight OIDC refresh joiners whenever refreshed access token
state changes, even if the IdP keeps the refresh token unchanged.

This prevents joined requests from retaining stale accessToken or
accessTokenExpiresAt values and redundantly refreshing later in the same run.

* 🌉 Persist OIDC refresh-token recovery bridges in MongoDB

Store SSE OBO refresh-token recovery bridges in MongoDB instead of
process-local memory so /api/auth/refresh can recover after worker
restarts or cross-worker routing.

Derive bridge expiry from REFRESH_TOKEN_EXPIRY so the recovery window
matches the stale refreshToken cookie it repairs, and delete bridges
after successful recovery.

* 🤝 Coordinate OIDC inline refreshes across workers

Add a short-lived Mongo-backed refresh-flight record so concurrent
OBO refreshes for the same OpenID session do not redeem the same
rotating refresh token on different workers.

The winning worker performs the IdP refresh and stores an encrypted
result; joiners wait for that result, hydrate their request session,
and return without calling the IdP.

* ⚓ Keep OpenID marker cookies aligned on inline refresh

Refresh token_provider and openid_user_id with the same expiry as the
rotated refreshToken cookie when an inline OBO refresh can still write
headers.

Share the marker-cookie writer with the normal OpenID auth refresh path
so the fallback /api/auth/refresh branch continues to recognize valid
OpenID refresh tokens after session expiry.

* 🔑 fix: include refresh token in OIDC local refresh flight key

Key the process-local OIDC refresh coalescing by the current session
refresh token, matching the Mongo-backed flight key. This prevents a
request with a newly rotated token from joining an older pending refresh
and inheriting its failure/result.

* 🌉 fix: store OIDC refresh bridge without cookie response

Treat missing or non-cookie responses like headers-sent streaming
responses during inline OIDC refresh. When the IdP rotates the refresh
token and cookies cannot be written, persist a recovery bridge so a later
/auth/refresh can recover after session expiry.

* 🫙 fix: preserve stale OIDC cookie bridge key

Track the refresh token last written to the browser cookie separately
from the current session refresh token. When inline OIDC refreshes rotate
tokens without a writable response, keep bridging from the browser-stale
token directly to the latest session token.

* 🙌 fix: keep OIDC bridge recovery success on cleanup failure

Make refresh-token bridge cleanup best-effort after a bridged OIDC
refresh succeeds. A transient delete failure now logs a warning but does
not convert the already-refreshed session and cookies into a 403 response.

* 📦 test: Exclude RefreshTokenBridge from tenant-isolation coverage

Add RefreshTokenBridge to the tenant-isolation coverage allowlist because
refresh bridge lookups run during unauthenticated OpenID refresh recovery.
The controller first recovers user context from the signed OpenID marker
cookie, then the bridge methods apply explicit user and tenant filters.

Ambient tenant isolation would bind this recovery path to request-local
tenant context that is not available at the point the stale cookie is being
resolved

* ⚡ Fix OpenID refresh flight retry and marker hydration

Allow failed OpenID refresh flights to be reclaimed immediately instead of pinning transient errors.

Preserve the browser refresh-token marker when joined refreshes hydrate session tokens from a shared flight result.

Stabilize AuthService tests by isolating mocked module imports from prior suites.

* 🛠️ fix: centralize OBO identity scoping

Add shared auth identity helpers for app user ids, OpenID subjects,
tenant ids, and normalized OpenID issuers.

Thread a non-placeholder-visible OBO identity context from the real
request user through MCP connection, tool-call, reinit, and refresh
paths. Keep tenantId and openidIssuer out of createSafeUser so MCP
user placeholders do not expose those fields.

Scope OBO token cache and in-flight exchange keys by tenant, issuer,
OpenID subject, scopes, and a SHA-256 hash of the upstream assertion.
This prevents cross-tenant/cross-issuer collisions and avoids reusing
tokens minted from stale rotated assertions.

Use the shared identity helpers for OpenID refresh-flight keys and
refresh-token bridge recovery records so related OBO refresh paths share
the same identity normalization rules.

The helper is intended for auth-boundary and credential-cache code, not
as a blanket replacement for ordinary app user id ownership checks.

* 🛠️ fix: preserve OIDC refresh-token sync on save failures

Sync OpenID refresh-token cookie/bridge state before persisting the
session so a transient session-store failure cannot lose an IdP-rotated
refresh token.

Also trigger sync when the session refresh token differs from the
browser refresh-token marker, not only when the current grant rotates
the token. This lets later writable refreshes repair stale browser
cookies left behind by SSE refreshes.

Route refresh bridge identity through the shared identity helper with
the threaded OBO identity context, falling back to request/user context
when needed.

Add regression coverage for session-save failures, stale browser cookie
repair, non-writable bridge storage, and shared-helper identity fallback.

* 🛠️ fix: keep OIDC refresh bridge during recovery grace

After successful bridged refresh recovery, re-store the stale-cookie bridge
with a short grace TTL instead of deleting it immediately. This lets parallel
/api/auth/refresh requests that already sent the stale browser cookie recover
before they can observe the first response's Set-Cookie.

Retarget the bridge to the refresh token returned by the bridged retry so
B-to-C refresh-token rotation remains recoverable. The grace TTL is parsed with
math() and defaults to 60s, which shrinks the replay window from the original
REFRESH_TOKEN_EXPIRY bridge lifetime to the short recovery grace period.

Remove the now-unused explicit bridge delete path from the service and
data-schemas method surface. Add regression coverage for grace re-store,
identity symmetry, retry failure behavior, and same-key upsert replacement.

* 🛠️ fix: fail closed on OBO MCP user identity mismatch

Add an OBO-specific guard before MCP tool execution that requires the
effective invocation user and captured request user to both have ids and
to match. This prevents OBO tool calls from falling back to a separate
configurable.user_id identity after request-bound OBO context has already
been captured.

Keep the existing user id fallback behavior for non-OBO MCP calls.

Tests cover mismatched OBO users, missing user ids, and the matching-user
path ignoring a conflicting configurable.user_id.

* 🛠️ fix: Guard OpenID bridge retry user identity

Extract the shared OpenID refresh/user-resolution flow in AuthController
so the normal refresh path and bridge-recovery retry use the same grant,
claims, issuer, user lookup, and diagnostic logging code.

Preserve the existing path-specific behavior: the normal path still owns
migration updates and 401 login redirects, while the bridge retry still
falls through to the existing 403 invalid-token response.

Add a bridge-recovery guard that rejects retry results whose resolved
user id differs from the signed openid_user_id cookie before issuing
tokens or re-storing the grace bridge. Cover both the successful
matching-user recovery and the mismatched-user rejection.

* 🛠️ fix: type-safety polish on OBO data layer

Replace refresh token bridge query/update Record<string, unknown> usage
with typed Mongoose FilterQuery and UpdateQuery definitions.

Harden OpenID marker cookie JWT expiry handling by converting refresh
expiry milliseconds to integer seconds and rejecting invalid or
non-positive durations.

Add focused CSRF tests for fractional refresh expiry values and invalid
expiry configuration.

* 🛠️ fix: Bind OpenID session tokens to authenticated identity

Stamp OpenID session token state with the LibreChat user id, OpenID subject,
tenant id, and normalized issuer when tokens are stored.

Fail closed before OBO inline token reuse/refresh when the session token
identity does not match the current authenticated identity, preventing a stale
or mixed Express session from supplying another user's upstream assertion.

Also validate the normal /api/auth/refresh session-token reuse shortcut against
the signed marker-cookie user before returning cached session tokens.

Note: sessions created before this change carry no identity stamp and are
treated as a mismatch. This is self-healing — the reuse path forces a full IdP
refresh (which re-stamps the session) and the OBO path throws, surfacing as a
one-time re-authentication for active OBO users at deploy time. The session
re-stamps within one session lifetime (SESSION_EXPIRY, default 15 min).

* 🛠️ fix: Recover OpenID refresh token drift

Prefer the browser refresh-token cookie when it differs from the
server-side OpenID session state, and force a real IdP refresh in that
case instead of reusing stale session tokens.

Store a short-lived refresh-token bridge when inline OBO refresh writes
a rotated browser cookie but session persistence fails, so follow-up
refreshes can still recover from the old token.

Keep the bridge grace TTL centralized in RefreshTokenBridge so both
recovery paths use the same env-backed value.

Note: drift is measured against the last-synced browserRefreshToken
marker, so the SSE path (intentionally stale cookie, authoritative
session) does not false-positive. Sessions predating the marker have no
browserRefreshToken; for those, drift falls back to comparing the cookie
against the session refresh token and prefers the cookie on difference.
This is the same self-healing pre-change-session window as the identity
binding fix and re-syncs within one session lifetime.

Tests cover cookie/session drift selection, reusable-session bypass on
drift, bridge storage after session-save failure, and the shared bridge
constant wiring.

* 🛠️ fix: Harden OBO token caching and expiry handling

Reject malformed OBO grant responses before writing them to the exchanged-token cache so a missing access_token cannot poison the cache.

Store absolute expires_at values with cached OBO tokens and ignore legacy cache entries without usable expiry metadata. This keeps cached-token freshness based on the token’s real
remaining lifetime instead of reusing the original relative expires_in on cache hits.

Move OBO expiry normalization and skew helpers into packages/api and use them from both the JS exchange service and the TS MCP resolver. Apply a 30-second safety margin with a one-
second floor for short-lived tokens, covered by direct helper tests and caller-level regression tests.

Tests:
- packages/api: npm run build
- packages/api: npx jest src/mcp/oauth/expiry.spec.ts src/mcp/oauth/obo.spec.ts
- api: npx jest server/services/OboTokenService.spec.js

* 🛠️ fix: Harden OBO refresh-token bridge lookup and indexing

Reuse getValidOpenIDReuseUserId for the bridge-recovery user lookup in
refreshController instead of re-verifying openid_user_id inline. The shared
helper enforces the JWT_REFRESH_SECRET presence check and a strict
typeof payload.id === 'string' guard, rejecting tokens whose id claim is
present but not a string (e.g. a numeric id) that the inline check accepted.

Fail closed on issuer mismatch in getRefreshTokenBridge. Both the stored and
the expected issuer are now normalized and compared for equality, so a bridge
is recovered only when both sides agree (both absent, or both present and
equal after normalization). Previously the check was skipped whenever the
stored issuer was absent, allowing recovery across mismatched issuer context.

Drop the unused {oldRefreshTokenHash, userId, tenantId, openidIssuer} index
and the openidIssuer field on RefreshTokenBridgeQuery. The data-layer filter
only queries the 3-field {oldRefreshTokenHash, userId, tenantId} index; the
issuer is verified in application code, not the query. Hoist the repeated
model accessor into getRefreshTokenBridgeModel.

Note: issuer is now load-bearing for recovery. A bridge stored with an issuer
recovers only when the lookup supplies a matching issuer; the recovery lookup
reads user.openidIssuer via AUTH_REFRESH_USER_PROJECTION (an exclusion
projection that retains the field). If a user's persisted openidIssuer is
empty while the stored bridge has one, recovery fails closed (falls through to
normal re-authentication) until the bridge TTLs out — no security regression.

Tests cover invalid signed-cookie payloads bypassing the bridge, both
asymmetric issuer-presence cases, issuer normalization before comparison, and
an index-alignment assertion guarding against re-adding the dropped index.

* 🛠️ fix: Degrade OBO discovery on token resolution failures

Catch expected OboTokenResolutionError failures during MCP tool discovery and
fall back to unauthenticated tool listing instead of aborting discovery. This
keeps discovery aligned with the existing unauthenticated listing behavior while
preserving unexpected errors as real failures.

Also correct OBO tool-call freshness comment and tighten the OBO trust-check
permissions type to the existing role permission shape.

Tests:
- npx jest src/mcp/__tests__/MCPConnectionFactory.test.ts --runInBand --coverage=false
- npx jest src/mcp/oauth/obo.spec.ts --runInBand --coverage=false

* 🛠️ fix: tighten OBO tool-call errors, bridge logging, and flight typing

Move resolveToolCallUserId inside the tool-call try/catch so an OBO
identity mismatch surfaces with serverName/toolName context and the
standard tool-call-failed message instead of an opaque bare Error.

Raise the refresh-token bridge lookup failure log from debug to warn so
transient infrastructure failures on the unauthenticated /api/auth/refresh
path are observable, and guard the message access against non-Error values.

Replace the unknown+cast in isDuplicateKeyError with a hasErrorCode type
predicate so the duplicate-key check reads error.code without an assertion.

Preserve real math/isEnabled in the MCPConnectionFactory test mock (mock
only processMCPEnv) so mcpConfig timeouts no longer resolve to NaN, fixing
the TimeoutNaNWarning that masked slow OAuth retry behavior.

* 🧪 fix: Restore the Flight Uniqueness Index and Buffer the Graph Cache TTL

Two CI failures on the merge, both in suites this environment cannot run
(their MongoDB binary download is blocked).

`GraphApiService.spec.js` still asserted the unbuffered TTL. Graph tokens
route through the same `getTokenCacheTtlMs` as the OBO and openidStrategy
caches, so the entry now expires 30s before the credential does.

`openidRefreshFlight.spec.ts` dropped the database between tests, which takes
the indexes with it, and Mongoose builds them only once when the model is
compiled. Whether the unique `key` index survived into a test was a race with
that one-time build. Without it a second `create` inserts instead of raising a
duplicate-key error, so every worker believes it won the flight — the
mutual exclusion the file exists to prove. Indexes are now rebuilt after each
drop, which also makes the reclaim and complete cases reach those paths for
the right reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: address OBO review findings

* 🔐 fix: Install Bridge Indexes and Carry OBO Through Assistant Recovery

Two findings from the Codex pass on d08c82f0d.

The refresh-token bridge relied on Mongoose auto-indexing for both of its
indexes, and `MONGO_AUTO_INDEX=false` is a supported deployment setting. A
bridge holds an encrypted refresh token and the TTL index is the only thing
that ever deletes one, so under that setting they would accumulate for the
life of the collection while concurrent upserts lost the compound uniqueness
the filter assumes. Installed before the first write, matching the flight
methods and the session and schedule methods before them.

`recoverServerTools`, the assistant create/update path that reruns
`reinitMCPServer` when a referenced server's catalog and connection snapshot
are both missing, was the last reinit site not carrying the upstream-token
closure. For an OBO server the factory rejects the connection outright, so the
assistant write failed with unavailable MCP definitions. It now builds the
provider at that request boundary like the other entry points; assistant
writes have no `res`, so a rotation there falls back to the recovery bridge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: harden OBO refresh coordination

* ⏳ fix: Keep Elapsed Expiries Elapsed and Revoke the Superseded Session

Two of the five findings from the Codex pass on fcdc15885 — the two that are
defects in code this branch introduced rather than design questions about the
bridge.

`getSkewedTokenExpiresAtMs` floored every result at a second in the future,
including an expiry the provider had already declared elapsed. An exchange
answering `expires_in: 0` or a past `expires_at` was handed to the MCP
connection stamped valid for another second, which only moves the failure
downstream. The floor now applies to a lifetime that is still live, which is
what it was for; an elapsed one stays elapsed so the caller rejects it. Same
for the cache TTL, which falls back to the elapsed-credential floor.

Bridge recovery left the stale token's durable Session behind. Only the token
it recovered through was passed as `existingRefreshToken`, so that one's
session was replaced while the token the browser actually presented kept its
record until its original expiry. That record, with the marker cookie still
bound to it, is what authorizes local image access for OpenID users — so a
copy of the stale cookie outlived the rotation it had lost. Revoked
explicitly on successful recovery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: close OBO refresh review findings

* 🎟️ fix: Carry the Bridged Token Through a Non-Rotating Recovery

Bridge recovery passes the browser's stale token as `existingRefreshToken` so
the durable Session naming it is the record replaced. That also makes the
stale token the fallback the installed session and the refresh cookie use when
a tokenset carries no `refresh_token` of its own, which holds only while the
recovery grant rotates.

An IdP that answers that grant without rotating sends the browser back to the
very token the bridge exists to retire: `storeOpenIDSession` installs and
deletes the same stale record in one call, and the cookie is rewritten to a
token the IdP already rejected — a sign-out on the next refresh. The grace
bridge one line above already guards this with `|| bridgedRefreshToken`; the
resolved tokenset now does the same, so leader and followers alike publish the
recovered token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* 🚫 fix: Reject an OBO Exchange That Returns an Expired Credential

Preserving an elapsed expiry through the skew helper only helps if something
acts on it, and nothing did: `MCPManager.callTool` checks the access token and
nothing else before setting the Authorization header, so a credential the IdP
declared spent still went downstream to fail there. It is rejected at the
exchange now, where the reason is known, and retryably — the exchange itself
worked, so a fresh grant can succeed.

Completes the elapsed-expiry change in cb26a6f7d, which made the stamp honest
without giving anyone a reason to look at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: close OpenID refresh review findings

* fix: coordinate OpenID refresh entry points

* chore: sort OpenID flight imports

* fix: fence OpenID refreshes during logout

* fix: close OpenID logout publication races

* fix: narrow completed refresh flight

* test: cover bridge cleanup failure after ownership loss

The compensating delete in storeRefreshTokenBridgeWithLease swallows its own
failure so the lease error stays the one the caller sees. Nothing asserted
that, so removing the inner catch left every suite green while callers began
receiving the cleanup error instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: compensate OpenID bridges only on proven ownership loss

The post-write lease assertion deletes the bridge it just published when it
throws, but it threw for two different reasons: a coordination record that is
no longer ours, and a coordination read that simply failed. Treating the second
as the first destroys the only mapping from the token the browser still holds
to the one the IdP already rotated to, so a transient Mongo error on the
headers-already-sent path signed the user out.

Tag the ownership error where the lease raises it and compensate only for that,
preserving the bridge whenever ownership is merely undetermined. A preserved
bridge stays behind the logout revocation fence, so the safe default costs
nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: reject spent OpenID refresh results

Two ways a refresh could report success while handing back a credential
nothing can use.

An inline refresh carries the previous id_token forward when the IdP omits
one on rotation, so tokenset.id_token is not necessarily freshly issued.
setOpenIDAuthTokens applied its freshness guard only to the session copy and
took tokenset.id_token unconditionally, so /refresh returned an expired
bearer even though the grant produced a usable access token. Skip it only
when it is provably expired: an id_token whose expiry cannot be read stays
preferred, since access_token may be opaque or scoped to another audience.

normalizeExpiresIn preserves a zero or negative lifetime rather than
discarding it, so a grant declaring an already-spent access token still
published, rotating the refresh token and returning a token every freshness
check rejects. Each OBO call then repeated the grant. Reject an elapsed
lifetime before publishing; an unknown lifetime still publishes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: harden OpenID refresh publication

* fix: keep identity and results intact through OpenID refresh cleanup

Two follow-ons from the last round's fixes.

Stripping an expired carried-forward id_token from the refresh result removed
the only identity material a rotation without id_token leaves behind. The
result is rebuilt by buildOIDCTokensFromSession, so it carries no provider
claims() either, and getTokenClaims accepts only those two — bridge recovery
failed with "no usable identity claims" before setOpenIDAuthTokens could hand
back the fresh access token. The stripped token now travels in a
non-enumerable marker, alongside the existing browser and predecessor markers,
which identity resolution reads and the authentication response never sees.

The lease drained a pending renewal by awaiting it in finally, so a transient
coordination failure there threw from finally and replaced the operation's
result. The refresh had already settled and published, so the caller saw a
failure on credentials that had rotated. Proven ownership loss is recorded on
ownershipLost and checked before the return, so the drain has nothing to add
but noise; it now absorbs and logs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* refactor: fence OpenID recovery publication

* fix: close OpenID publication transaction

* refactor: make OpenID publication transactional

* fix: satisfy OpenID publication type checks

* fix: fence OpenID session publication

* fix: authorize OpenID refresh publication

* fix: bind OpenID replay generations

* fix: fence OpenID response generations

* fix: authorize OpenID token delivery

* fix: linearize OpenID publication delivery

* chore: sort OpenID refresh flight imports

---------

Co-authored-by: J.C. Bartle <jcbartle@users.noreply.github.com>
Co-authored-by: jbartle <jbartle@rand.org>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: jcbartle <7274202+jcbartle@users.noreply.github.com>
2026-08-29 23:55:58 -04:00
Danny Avila
10981b8637
🍱 feat: Guide Compaction With a Bounded Semantic Index (#15340)
* 🧭 feat: guide compaction with semantic context

* 🛡️ fix: fail closed on semantic intent collisions
2026-08-29 17:53:04 -04:00
Danny Avila
773127bff2
🎠 refactor: Route Every Event Actor Turn Through One Lifecycle (#15325)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* refactor: unify Event Actor turn lifecycle

* fix: retain Event Actor fence ownership

* fix: preserve mixed-version actor suspension safety
2026-08-28 17:30:23 -04:00
Danny Avila
3fa33b740b
🛫 refactor: Promote Generation Protocol V2 Automatically (#15324)
* refactor: promote generation protocol v2 automatically

* fix: remove unused protocol import
2026-08-28 17:17:09 -04:00
Danny Avila
77c2a51cf3
🪧 fix: Advertise Detached Event Actor Support from the Generation Store (#15322)
Some checks failed
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Has been cancelled
Frontend Unit Tests / Build packages (push) Has been cancelled
Frontend Unit Tests / TypeScript type checks (client) (push) Has been cancelled
Frontend Unit Tests / Tests: @librechat/client (push) Has been cancelled
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Has been cancelled
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Has been cancelled
Frontend Unit Tests / Vite build verification (push) Has been cancelled
* fix: activate detached event actions automatically

* fix: shield detached terminal generations

* perf: share capability availability index

* style: flatten capability status selection

* fix: preserve mixed-version lifecycle shells

* fix: complete detached action store adapters

* fix: close mixed-version capability races

* fix: honor legacy capability success
2026-08-28 16:33:23 -04:00
Danny Avila
487984193a
🪂 feat: Make Event Actor Detached Actions Durable (#15307)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* feat: make Event Actor detached actions durable

* fix: align detached actor checks with merged base

* fix: close detached actor lifecycle gaps

* fix: break detached action outcome cycle

* fix: deepen detached action ownership

* fix: fence detached launch handoffs

* fix: preserve detached rollout fencing

* test: type detached recovery failure

* fix: close detached ownership handoffs

* fix: stage detached action activation

* fix: persist detached terminal retries

* fix: fence detached durability ownership

* fix: fence trigger lane publication

* fix: fence detached transition generations

* fix: retire failed detached predecessors

* fix: close detached rollout consumers

* test: type detached retry turns

* fix: close detached ownership gaps

* fix: fence detached action resumes

* fix: fence prior-head event actor resumes

* fix: preserve legacy event actor resumes

* fix: align detached resume boundaries
2026-08-28 14:21:04 -04:00
Danny Avila
c06b09c945
🤖 feat: make Event Actor HITL durable (#15305)
* 🤖 feat: make event actor HITL durable

* 🤖 fix: break event actor outcome cycle

* 🤖 fix: close durable actor terminal races

* fix: harden durable event actor recovery proofs

* fix: close event actor resume publication races
2026-08-28 08:05:11 -04:00
Danny Avila
6fba51a35a
🫆 feat: Fingerprint Agent Context for Zero-Read Warm Continuation (#15299)
* feat: add context-fingerprinted warm continuation

* fix: preserve manual skills across warm turns

* perf: keep ordinary agent initialization neutral

* fix: fingerprint complete agent topology

* fix: close warm context replay gaps

* fix: preserve lazy subagent memory scope

* perf: fingerprint only model-bound memory

* fix: project lazy memory capability

* style: sort warm continuation imports

* fix: make warm continuation reconstructable

* fix: preserve lazy skill isolation

* fix: retain scoped skill semantics
2026-08-27 23:01:14 -04:00
Danny Avila
a5fd00cd7a
🎼 feat: Unify Agent Turn Execution Behind One Immutable Plan (#15296)
* feat: unify agent turn execution planning

* fix: preserve automatic subagent activity handles

* fix: gate subagent wakeups for ephemeral parents

* fix: scope subagent wakeup guidance by agent
2026-08-27 22:26:46 -04:00
Danny Avila
f4efef4b3a
♟️ fix: Settle First Bound Actor Events (#15300) 2026-08-27 19:06:41 -04:00
Danny Avila
03fa98eb37
🕸️ feat: Complete Keenable Web Search Provider (#15288)
* feat: wire Keenable web-search provider into config, schema, and UI

Keenable landed as a search provider in @librechat/agents (#285, shipped in
3.2.58+), but LibreChat did not yet expose it. This adds the config/schema/UI
glue so it can be selected, mirroring the existing Tavily provider.

- data-provider: add `keenable` to SearchProvider type + SearchProviders enum,
  keenableApiKey/keenableApiUrl schema fields, and a keenableSearchOptions block
  (maxResults, site, attributionTitle, timeout).
- data-schemas: register keenable in webSearchAuth.providers and default the
  key/URL placeholders in loadWebSearchConfig.
- api/web: pass keenableSearchOptions through to the provider and handle
  Keenable's keyless model. Unlike other providers it authenticates with no key
  (the public endpoint), picking up an optional key/URL when set; the URL
  override is SSRF-preflighted like other user-provided URLs.
- client: add Keenable to the provider dropdown with an optional API-key input.
- docs: document KEENABLE_API_KEY/KEENABLE_API_URL in .env.example and a
  webSearch example in librechat.example.yaml.
- tests: keyless + keyed auth resolution, config defaults, and schema parsing.

* fix: ESLint no-unused-vars and clarify Keenable yaml example

- Remove the now-unused RerankerTypes import in data-schemas web.ts (the lint
  job runs with --max-warnings 0 on changed files, so this latent warning failed
  CI once the file was touched).
- Note in the librechat.example.yaml Keenable stanza that a scraper (and
  reranker) is still required for web search to load, and include a Firecrawl
  scraper in the example.

* chore: fix import order drift (sort-imports)

* feat: add Keenable as a keyless scraper and select it without a pinned provider

The Keenable scraper landed in @librechat/agents#337, so wire the scraper
category the same way the search provider already is: `scraperProvider:
keenable` reads pages through Keenable's public fetch endpoint with no key
(a key only lifts rate limits, and the endpoint is overridden with
KEENABLE_FETCH_URL). Paired with `rerankerType: none` this makes a fully
keyless web-search stack possible for the first time.

Also closes the Codex finding on this PR: because none of Keenable's auth
fields are required, the generic auth loop skips it whenever it isn't pinned,
so a key submitted through the API-key dialog (which cannot pin a provider)
left the providers category unauthenticated. Keenable is now selected in that
case, gated on one of its values actually being present so installs that
configured nothing keep their current behavior. The scraper gets the same
fallback, additionally gated on Keenable being the resolved search provider,
so it never silently scrapes for another provider.

* fix: select the Keenable scraper from a supplied key, not only for Keenable search

The API-key dialog submits credentials and cannot pin a provider, so choosing
Keenable as the scraper while search stays on Serper/SearXNG/Tavily had no
effect: the unpinned-scraper fallback required Keenable to also be the resolved
search provider.

A supplied Keenable value now triggers it as well, which is the only signal the
dialog can send. The fallback still runs only when no keyed scraper
authenticated, and with neither trigger the category stays unauthenticated, so
a deployment that never configured Keenable is unaffected.

Note the fully keyless choice still cannot be expressed through the dialog:
Keenable's key is optional, so picking it with no key submits nothing at all.
librechat.example.yaml now documents pinning scraperProvider: keenable for that
case.

* style: Sort Keenable imports

* fix: Harden Keenable auth resolution

* fix: Preserve Keenable selection intent

* fix: Fail closed on invalid web search auth

* fix: close keenable auth gaps

* style: sort web auth imports

* fix: preserve web search selection integrity

* fix: isolate web search auth ordering

* fix: silence expected credential misses

* chore: bump agents sdk

* fix: preserve web search preference ownership

* fix: forward cleared Keenable endpoint

* style: sort web search hook imports

* fix: require intent for credential clears

---------

Co-authored-by: Ilya Bogin <ilya.bogin@keenable.ai>
2026-08-27 14:29:20 -04:00