mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-11-01 11:36:29 +00:00
1625 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
be5b5ca92a
|
test: stabilize full test suite | ||
|
|
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 |
||
|
|
54511b2b8c
|
🧠 fix: Return 400 for Invalid Memory Keys (#14618)
* fix: return 400 for invalid memory key format instead of 500
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: trim PATCH rename key to match POST normalization
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: return 400 when PATCH rename key is not a string
A non-string `key` in the PATCH body (e.g. {"key": 123}) previously fell
back to the URL key and updated the memory with a 200, hiding the invalid
field. Reject any defined non-string key with 400 before normalization.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: share memory key validation between schema and routes
* fix: reject blank memory rename keys
* test: use real data-provider exports in memory route suite
* fix: share memory key validation with ID updates
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
|
||
|
|
000349681e
|
🩻 feat(insights): Scope Insights to Authorized Agents (#15549)
* 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 |
||
|
|
57c52de33c
|
🛎️ fix: Start Code Environment Reconciliation After Mongo Connects (#15620) | ||
|
|
3d085b227d
|
🛰️ feat: Show BYOM Worker Readiness (#15618)
* feat: Show BYOM worker readiness * test: Exercise approved BYOM commands end to end * test: Wait for BYOM worker readiness |
||
|
|
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. |
||
|
|
fe30e3db32
|
🪫 fix: Back Off Failed Retention Deletes (#15608)
* 🧹 fix: Stop Undeletable Files Starving the Retention Sweep `getExpiredFiles` returns the oldest `expiredAt` first, capped at `limit`, and `processDeleteRequest` leaves the record in place when storage deletion fails. Nothing records the failure, so the same files come back at the head of the next batch an hour later, forever: no backoff, no cap, and — once `limit` of them cannot be deleted — no file that expires afterwards is ever swept again. On the deployment behind #15511 that is ~29k stranded objects permanently occupying a 100-slot queue, which is why fixing the Code Interpreter side alone (LibreChat-AI/code-interpreter#85) would not have resumed deletion there. Failures are now recorded on the file. `deletionRetryAt` holds it back with a backoff doubling from one sweep interval to a day, and `deletionAttempts` retires it from the query entirely once it reaches `FILE_RETENTION_SWEEP_MAX_ATTEMPTS` (10, so roughly five days of retries). Both fields are absent on existing records and absence means "never attempted", so nothing already in the collection changes eligibility. The record itself is kept rather than deleted — the reference is what an operator needs to reconcile a bucket the sweep could not clear, and dropping it would restore the silence that made this leak invisible. Raising `FILE_RETENTION_SWEEP_MAX_ATTEMPTS` re-admits everything previously given up on, which is the supported way to resume once the storage-side failure is fixed; the give-up is logged with that instruction. The counter is incremented server-side with `$inc` so concurrent sweeps on separate nodes cannot overwrite each other's progress toward the cap, and a failure to record a failure is logged and skipped rather than aborting the rest of the batch. * 🧭 fix: Delete Code Environment Files Through the Route That Exists `deleteCodeEnvFile` tried `/sessions/:sid/objects/:fid` before falling back to `/files/:sid/:fid`. Both have been there since #13424, but only the second is mounted by any released codeapi — the first gained DELETE in LibreChat-AI/code-interpreter#85 — so every deletion paid a guaranteed 404 and a wasted round trip, and #15511 read that 404 as the whole bug. Call `/files/:sid/:fid` directly. It is the safe direction to collapse toward: codeapi has mounted it since its first release, so this works against older deployments as well as post-#85 ones, whereas keeping the other path would not. Collapsing the loop tightens two behaviours that only existed to serve it. A 405 now surfaces instead of being swallowed on the way to a second attempt; there is no second route to try, and a service that refuses the method should say so. A 404 is still treated as "already gone" — that is the only thing it can now mean — but it is logged rather than passed over in silence, because a 404 caused by a misconfigured base URL looks identical and this branch drops the file's metadata record either way. * 🔒 fix: Settle sweep retry state from the write, not the read Two findings from the review of |
||
|
|
c302ae6f35
|
🪁 fix: Honor Endpoint Drop Params in Parameter Panels (#15610)
* Remove parameters from UI when parameter is dropped through dropParams * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Addressed copilot and codex review suggestions * 1. Normalize drop parameter names in client/src/components/SidePanel/Parameters/Panel.tsx Admin-configured dropParams for Azure/OpenAI-compatible custom endpoints use the effective backend field names (maxTokens, topP, frequencyPenalty, presencePenalty), but the panel's filter compared them against the UI's snake_case keys (max_tokens, top_p, etc.), so the controls stayed visible and silently discarded whatever the user set. Added a dropParamsBackendToUIKey map (in packages/data-provider/src/parameterSettings.ts) and normalize each dropParams entry through it before filtering. 2. Apply the same drop-parameter filtering in client/src/components/SidePanel/Agents/ModelPanel.tsx The Agent builder's model panel built its parameter list independently and never consulted endpointsDropParamsMap at all, so it kept offering controls for Azure/custom providers whose values the backend drops. Reused the same resolution logic (including the new backend→UI key normalization) there, sourced from useGetStartupConfig. 3. Remove any escapes from Azure fixtures in packages/api/src/app/config.test.ts Three tests built azure groupMap/modelGroupMap fixtures via ... as any as AppConfig['endpoints'], bypassing type checking for that shape. Replaced them with two typed helpers, createAzureGroupMap and createAzureConfig, built from the existing TAzureConfig/TAzureGroupMap/TAzureModelGroupMap types, so the fixtures are now fully type-checked with no any. * HAL-1081 Deploy librechat open 1. Gate the backend-name alias to OpenAI-compatible parameter sets (packages/data-provider/src/parameterSettings.ts) dropParamsBackendToUIKey had been applied unconditionally, rewriting a dropped topP to top_p even for a custom endpoint whose defaultParamsEndpoint is anthropic/google (or a native bedrock-* endpoint) — where topP is the UI key, so the rewrite broke hiding the control. Replaced the plain map with resolveDropParamsUIKeys(dropParams, endpointKey), which only aliases backend names for OpenAI-compatible endpoint keys (openAI, azureOpenAI, custom, openRouter) and passes native-provider keys through unchanged otherwise. Updated both Panel.tsx and ModelPanel.tsx to call it with overriddenEndpointKey, and added a resolveDropParamsUIKeys test suite in parameterSettings.spec.ts. 2. Prune stale model_parameters when a control becomes hidden (client/src/components/SidePanel/Agents/ModelPanel.tsx) When a parameter an agent already had set gets added to its endpoint's dropParams, the control disappeared but the value stayed in model_parameters and was saved unchanged by composeAgentUpdatePayload — invisible to the user and silently reactivated if the endpoint later stopped dropping it. Added a useEffect, mirroring the conversation panel's existing pruning effect, that strips any model_parameters key no longer present in the currently-visible parameters list. Also fixed a pre-existing test regression (missing useGetStartupConfig mock) and added two tests covering pruning-on-drop and value retention when the control stays visible. * fix: Preserve parameters for unknown agent providers * fix: Sanitize agent parameters on submission * fix: Preserve agent model overrides during pruning --------- Co-authored-by: Marc Amick <MarcAmick@jhu.edu> Co-authored-by: MarcAmick <5194465+MarcAmick@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
09da878a47
|
🚰 fix: Throttle Shared Link Reads and Aggregate Audit Findings (#15605)
* 🪵 fix: Aggregate Audit Findings and Rate Limit Shared Link Reads Audit-mode content filter rules wrote one `logger.info` per matching fragment, and inspection deliberately continues past every finding. A shared conversation full of distinct matching strings therefore produced one log write per fragment, regenerated on every retrieval of `GET /api/share/:shareId` — a public, unauthenticated route with no limiter. The 4,096-entry inspection dedupe set is a memory bound, not an event budget: once full, further values are inspected and logged again. Audit metadata carries no inspected text — only rule, source, field and provenance — so thousands of those writes were byte-identical lines. Findings are now counted inside an aggregation scope and reported once per distinct key with an `occurrences` count when the scope ends: - `assertModelBoundContent` opens a scope when audit rules are configured, bounding one inspection pass (a single message's fragments). - `assertConversationImportContentAllowed` opens the outer scope for import and shared-link snapshots, so per-message inspections nest into one report instead of one per message. - `createShareContentPreflight` spans the shared-file metadata pass too. Nested scopes reuse the outermost aggregation, and outside any scope the previous immediate write is kept. `GET /api/share/:shareId` re-inspects the whole snapshot on every request, so it now carries IP and user limiters like the neighbouring fork route (`SHARE_IP_MAX`/`SHARE_IP_WINDOW`, `SHARE_USER_MAX`/`SHARE_USER_WINDOW`, `SHARE_VIOLATION_SCORE`); the user limiter skips anonymous viewers. * 🔒 fix: Address Codex Findings on Share Audit Aggregation - The aggregation key joined rule fields with a space, but the filter schema allows spaces in custom pattern ids and labels, so `{id: 'a', label: 'b c'}` and `{id: 'a b', label: 'c'}` collided on the same source/field/provenance and merged two rules' counts under one identity. Serialize the tuple instead. - `SHARE_VIOLATION_SCORE` left unset reached `logViolation` as `undefined`, which selects that function's default score of 1 rather than the documented zero. With default ban settings a viewer refreshing a rate-limited shared link would be banned after 20 rejections. Default the score to 0 in code, not only in `.env.example`. - `removePorts` returns a full IPv6 address, so an attacker holding a prefix could rotate the host portion for a fresh bucket on every request — and for anonymous viewers the IP limiter is the only bound. Derive the key through `ipKeyGenerator` so IPv6 clients group by /56. |
||
|
|
7d9db14ba5
|
📓 feat: Author Files in Attached Workspaces (#15548)
* feat(code): route file mutations to attached workspaces * fix(code): Inspect attached edits before commit * fix(api): Annotate workspace limits * fix(code): Normalize workspace edit previews * style(api): Sort workspace imports * fix(api): Preserve Workspace List Validation |
||
|
|
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. |
||
|
|
a21c7944ee
|
🏷️ fix: Record the Media Type of Saved Images, Not the Declared One (#15606)
`saveBase64Image` recorded the media type declared in the incoming data URL
while persisting bytes that sharp had re-encoded. Those are not always the same
format: sharp rasterizes SVG to PNG, so an image arriving as `image/svg+xml` was
stored as `<id>-<name>.svg`, typed `image/svg+xml`, holding PNG bytes.
That type is not cosmetic. `encode.js` hands `file.type` to providers verbatim —
`media_type` for Anthropic, `inlineData.mimeType` for Google, and the `data:`
prefix for OpenAI — so re-attaching such an image sends PNG bytes under a media
type the provider does not accept for images, and the request fails.
A declared type is also only ever a claim. The same mismatch arises whenever a
producer mislabels what it sends, which for MCP tool output means any connected
server can decide what the record says about bytes it did not have to match.
`resizeImageBuffer` already read the encoded output back to measure it, so it now
resolves that metadata to a media type and returns it, and `saveBase64Image`
records it in place of the declared one, falling back to the declared type only
when sharp reports a format that has no media type of its own. The upload paths
were already correct — they record `image/${imageOutputType}`, the format they
convert to — so this brings the tool-output path in line with them.
`resolveImageMimeType` lives in `packages/api` because AVIF and HEIC share the
heif container and are told apart by its compression, which is worth stating once
with tests rather than open-coding at each call site.
|
||
|
|
fddf7e31f2
|
🛻 feat: Route Bash Through Attached Workspaces (#15546)
* feat(code): Route bash through attached workspaces * fix(code): harden attached bash schema adapter |
||
|
|
e23e13e712
|
🌳 feat: List Attached Workspace Trees (#15527)
* feat(code): list attached workspace files * test(code): include workspace list history * fix(code): validate workspace tool scope consistently * fix(code): Harden workspace listings * fix(code): Align workspace list validation * fix(code): Continue complete workspace listings * fix(code): preserve canonical path protections * test(code): keep workspace list specs type-safe |
||
|
|
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 @import (or @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 |
||
|
|
9a64e85b1e
|
🕯️ feat: Search Text in Attached Workspaces (#15525)
* feat(code): search attached workspaces * fix(code): remove duplicate workspace type import * fix(code): abort attached workspace searches * docs(code): correct workspace abort parameters * test(code): include workspace search history * fix(code): keep workspace search foregrounded * fix(code): preserve attached search routing * fix(code): Bound workspace search output * style(code): Format workspace search output |
||
|
|
b7fd4ba2a2
|
👓 feat: Read Attached Workspace Files (#15524)
* feat(code): read attached workspace files * style(api): sort workspace handler import * fix(code): abort attached workspace reads * test(code): type workspace fetch doubles * style(code): sort workspace bridge imports * fix(code): Preserve workspace read boundaries * style(code): Flatten workspace line validation * fix(code): Bound attached workspace responses * fix(code): Require canonical workspace paths * fix(code): fence workspace bridge operations |
||
|
|
5840e2f2ae
|
🪓 feat: Add Tenant-Scoped Agent Deletion (#15573)
* feat: add Agent Management delete endpoint * fix: scope agent deletion cleanup by tenant |
||
|
|
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. |
||
|
|
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 |
||
|
|
9a43362663
|
🪤 chore: Scope Assistant Tool-Resource Lookups (#15579)
`DELETE /api/files` looked the assistant up by `id`, but the Assistant schema
stores it as `assistant_id`. `api/db/connect.js` sets `strictQuery`, under which
Mongoose drops filter keys absent from the schema — so the lookup degraded to
`findOne({})` and returned whichever assistant happened to be first in the
collection, regardless of owner.
The unlink path then read that stranger's `tool_resources` and intersected it
with the caller's requested files, so a file the caller named could be unlinked
on the strength of another user's assistant record. With an empty collection the
same lookup returned `null` and the unguarded `assistant.tool_resources` threw,
answering 400 instead of completing the request.
Filter on `assistant_id` and guard the dereference. Adds three route tests that
mirror the server's `strictQuery` setting, without which the harness cannot
reproduce either failure.
|
||
|
|
78b320cbbc
|
🗜️ fix: Bound Conversation Page Size (#15576)
`GET /api/convos` read its page size as `parseInt(req.query.limit, 10) || 25` and handed it to `getConvosByCursor` unclamped, where it becomes `.limit(limit + 1)`. `?limit=-1` therefore issued `.limit(0)`, which MongoDB reads as no limit at all, and the route returned the caller's entire conversation list in one response; `?limit=999999999` forced the same unbounded fetch. `attachSharedFlags` then amplified it with a second query whose `$in` carried every returned id. Every other list route already clamps, so the fix reuses that shape: the projects handler's local `normalizeLimit` moves to `packages/api/src/utils/query` and both routes share it, and `getConvosByCursor` bounds its own page size so the invariant holds for any caller. |
||
|
|
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> |
||
|
|
2fe872d2ea
|
👀 feat: Add Agent Management Read Endpoints (#15561)
* feat: add Agent Management read endpoints * fix: protect Agent management configuration reads * fix: type Agent management list filters * test: use typed Agent management assertions |
||
|
|
a6cf61abcf
|
🪵 feat: Add OpenTelemetry Logs Pipeline via Winston Bridge (#15565)
Exports application logs as OTel log records when OTEL_LOGS_ENABLED=true, correlated with the active trace, and gates every signal explicitly so a disabled signal never falls back to the SDK's default OTLP exporter. Verified end to end over OTLP/gRPC against an in-process collector. |
||
|
|
aec2121923
|
🔐 feat: Add M2M Authentication for Agent Management (#15555)
* feat: add M2M authentication for Agent Management * fix: Harden Agent Management authentication * fix: Preserve OIDC authentication safeguards * fix: Normalize management principal IDs * fix: Bound OIDC signing key lookups * fix: Allow non-browser Agent Management clients |
||
|
|
2d794a0439
|
💓 fix: Detect Silently Dead Redis Sockets With Deadline-Bounded Heartbeats (#15539)
* fix: Detect silently dead Redis sockets with deadline-bounded heartbeats A Redis peer can vanish without a FIN or RST (a dropped NAT entry, a proxy failover, a migrated VM). The socket stays `ready` from ioredis's point of view, no error fires, and every command on it waits for the kernel's retransmission timeout — about fifteen minutes at Linux defaults. Observed in production: while the main client's socket was dead, POST /api/agents hung until Cloudflare returned a 524; while the subscriber's socket was dead, every attach failed with "Timed out synchronizing Redis subscription" for sixteen minutes across five conversations. `REDIS_PING_INTERVAL` could not help — it awaits ping() with no deadline, so the probe just joins everything else that is waiting. - Add startRedisHeartbeat: races a PING against a deadline and destroys the socket when the probe goes unanswered, so ioredis reconnects, replays queued commands, and re-subscribes a subscriber's channels. - Destroy the stream rather than disconnect(true), whose FIN a vanished peer never acknowledges; cluster clients fall back to the regular reconnect. - Add createIoRedisSubscriber so every dedicated subscriber gets an error listener and its own heartbeat, and route the generation-stream and both subagent-routing subscribers through it. - Set ioredis keepAlive from REDIS_KEEP_ALIVE (default 10s). - Add REDIS_PING_TIMEOUT, REDIS_SUBSCRIBER_PING_INTERVAL and REDIS_KEEP_ALIVE, documented in .env.example. - Give the fake RESP server a silent mode and a SUBSCRIBE reply so the dead-peer case can be tested against real clients. * fix: Probe each cluster node and refuse a non-positive heartbeat deadline A cluster-level PING is routed to one node, so a healthy reply proved nothing about the other sockets. The heartbeat now resolves its targets each tick — every node for a cluster, the client itself otherwise — and races an independent deadline per target, tearing down only the socket that stops answering. A non-positive deadline disables the heartbeat with a warning instead of forcing a reconnect on every tick, and the single-probe test no longer depends on wall-clock slack. |
||
|
|
d346e5ab5e
|
🚰 fix: Record the Provider Drain When Shutdown Cuts a Generation Short (#15532)
* 🚰 fix: Record the Provider Drain When Shutdown Cuts a Generation Short
* fix: Await generation settlement on shutdown instead of publishing the drain
* fix: Spend the real shutdown budget and cover the initialization window
* fix: Keep the shutdown registration literal the index spec asserts on
* fix: Keep draining settlements registered while shutdown is waiting
* fix: Track provider executions at the begin/drain chokepoints instead of per caller
* fix: Open the provider tracker before the begin CAS and cap cluster workers to the primary deadline
* fix: Propagate the cluster deadline and release trackers on reconfigure and abandonment
* fix: Hold SIGTERM until the worker acknowledges the cluster deadline
* fix: Refuse provider starts after shutdown begins and harden the cluster deadline handoff
* test: Assert the multi-execution shutdown wait with both executions begun before shutdown
|
||
|
|
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> |
||
|
|
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. |
||
|
|
0219513115
|
🛑 fix: Forward Run Abort Signal to Foreground Tool Calls (#15517)
* 🛑 fix: Forward Run Abort Signal to Foreground Tool Calls Stopping a generation left in-flight tool calls running on the far side. For MCP over streamable-http the external server kept burning CPU until the tool finished or timed out, with no `notifications/cancelled` ever sent. The MCP layer was never the problem: `MCPManager.callTool` already spreads `options.signal` into `client.request`, and the SDK's `Protocol.request` sends `notifications/cancelled` the moment that signal aborts. The signal simply never arrived. Agent tools do not execute inside LangGraph's ToolNode — `eventDrivenMode` dispatches `on_tool_execute` and `createToolExecuteHandler` invokes them. The agents SDK puts the run's abort signal on the batch request (`ToolExecuteBatchRequest.signal`, documented as one handlers should forward), but the handler destructured every field except `signal` and built its invoke config without it. `config.signal` was therefore `undefined` in every foreground tool, so `createMCPTool` derived no signal and the SDK registered no abort listener. Forward it. This restores cancellation for all signal-aware foreground tools, not just MCP. The detached background invoke keeps its own controller, since that work deliberately outlives the turn. An aborted call now rejects promptly and resolves an error result rather than hanging, so the abort is logged at debug instead of as a tool error — a user pressing Stop should not spray the error log. * 🔇 fix: Keep Cancelled Tool Calls on the Filtered Path and Out of Error Logs Addresses both findings from the Codex review of |
||
|
|
0b96fb9f70
|
🪪 feat: Propagate Tenant to Agent Model Headers (#15521)
* fix: propagate tenant to agent model headers * fix: propagate tenant across agent model calls |
||
|
|
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 |
||
|
|
b1c56239f9
|
🧤 feat: Enforce Safe BYOM Tool Approvals (#15503)
* feat: add safe default approvals for BYOM code * fix: preserve safe BYOM approval baseline |
||
|
|
6372cb08ee
|
🧶 fix: Untangle Parallel Subagent Descriptor Paths (#15492)
* fix: isolate parallel subagent descriptor walks * fix: preserve canonical subagent config identity * fix: separate subagent graph assembly from path validation |
||
|
|
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. |
||
|
|
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 |
||
|
|
0f9667b391
|
🔎 fix: Respect Search Page Limits in MeiliSearch Queries (#15482)
* fix(api): align message search with page size Pass a positive, capped page size to MeiliSearch and use the same conversation limit so requested message search pages are not silently truncated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(data-schemas): expand shared-link search results Request the MeiliSearch result ceiling using only conversationId so cursor pagination is not limited to the default 20 hits or burdened with unused fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
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. |
||
|
|
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 |
||
|
|
5e49868f0a
|
🔑 fix: Decode Action OAuth Credentials After Decryption (#14698)
* 🔑 fix: Decode Action OAuth Credentials After Decryption Action client credentials are URL-encoded before encryption, but the OAuth token exchange decrypted them with `decryptV2` alone, sending values like `abc%40domain.com` to the provider and causing 401s for secrets containing `@`, `+`, `=`, `/` or `:`. Decoding falls back to the raw decrypted value so legacy secrets stored before |
||
|
|
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 |
||
|
|
6195273429
|
🧲 fix: Anchor Memory Partitions to Accessible Agents (#15475)
* fix: validate agent memory partitions * fix: authorize agent memory mutations * test: cover agent manager memory access * fix: centralize agent memory authorization * fix: harden agent memory authorization * fix: support synchronous role providers |
||
|
|
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 |
||
|
|
c8939c9730
|
🪄 fix: Reuse Downloaded Artifacts for Image Reads (#15454) | ||
|
|
8f6228095f
|
🖼️ fix: Stop Reporting a Failed Sandbox Image Read as an Unreadable Format (#15453)
* 🖼️ fix: Stop Reporting a Failed Sandbox Image Read as an Unreadable Format `read_file` on a `/mnt/data` image collapsed every failure — rate limit, missing path, truncated transfer, transport error — into the binary-file hint, whose image branch reads as a permanent capability limit ("is an image file and cannot be read as text"). The model stopped retrying, and the operator had no way to tell which failure had actually occurred. The failure that surfaced it: the reader windows the bytes through `/exec` stdout, one call per window, against the Code API's per-user execution limiter (20 requests / 30s in the reference deployment). At a fixed 32KB window a 122KB chart cost 4 calls and a 1MB image cost 33 — more than a whole limiter window, so anything over ~640KB could never complete. A turn that generates several charts and reads them back exhausts the budget partway through and reports the 429s as unreadable images. - `handlers.ts`: failed reads name the real cause and the affordance that matches it — rate-limited waits and retries, a missing path lists the directory, a truncated transfer retries. The "cannot be read as text" phrasing is now reserved for bytes that are not a supported image. An image past the inline budget says to downscale and read the smaller copy rather than pointing at `bash_tool`, which cannot show an image. - `process.js`: the window is derived from the runner's stdout budget (48,960B vs 32,768B, so a 122KB chart is 3 calls and 1MB is 22), halves and re-reads the same offset when a runner truncates anyway (remembered per Code API, with the env remedy logged), waits out a rate limit it can afford instead of discarding the bytes already pulled, and stops at a 24-call ceiling so no read stalls a turn across limiter windows. - `utils/code.ts`: shared `getCodeApiRetryAfterMs` (header, then the `retry_after_seconds` body); `skillFiles.ts` drops its private copy. * ♻️ refactor: Move the Sandbox Image Reader Into TypeScript Addresses the review round on #15453. - P1: windowing, window sizing, the in-sandbox reader script, response parsing and byte assembly move to `packages/api/src/files/code/image.ts`, and the Code API rate-limit policy to `packages/api/src/utils/code.ts`. `process.js` keeps only the `/exec` transport it owns. Both new modules are tested against their real implementations (19 + 13 cases), including the round-trip ceiling, the changed-file guard and the shell-injection guard on the reader script. - P2: a configured stdout budget below the old 8KB window floor produced a window the runner could never emit, and narrowing was pinned to the same floor, so no retry could succeed. The window now aligns to the base64 group size instead, and the round-trip ceiling bounds what a small budget costs. - P2: `.env.example` documented a 32,768-byte default that no longer holds and never mentioned the stdout budget. It now describes `LIBRECHAT_CODE_SANDBOX_OUTPUT_MAX_SIZE` as the setting to tune, with the chunk override as the escape hatch. - P3: `process.spec.js` no longer reimplements the retry parser in its module mock. Behaviour is tested where it lives; the mock is a declared passthrough and the spec covers what the transport still owns — session forwarding, the profile header, parser hand-off and error logging. - P3: `getCodeApiRetryAfterMs` reads the response body through `in` narrowing rather than an `as` assertion. - P3: the window-sizing JSDoc no longer claims a multiple of 3 in the one case that is not — an explicit `LIBRECHAT_CODE_IMAGE_CHUNK_BYTES`. * 🎯 fix: Size Sandbox Image Reads to What the Limiter Can Deliver Addresses review round 2 on #15453. - Narrowing now halves the caller's own failing window instead of the shared learned value, and the learned value only moves down to the narrowest size any read needed. Reads run concurrently (one per tool call), so two reads failing at the same size were compounding into a window two steps smaller than the runner would have accepted, and persisting it for every later read. - Narrowing continues until a runner accepts a window or a floor below which no runner could serve an image at all. Two halvings stopped at 12,240 bytes — still ~16KB of base64 — so an 8-10KB stdout cap that was not configured up front stayed unreachable. - The round-trip ceiling is now one limiter window (20), and a read that cannot finish within it says so from the FIRST response rather than spending the whole budget rediscovering it: the first window reveals both the file size and what a call really delivers. A near-limit image no longer drains the turn's executions before failing, which is what made the wait budget look too small. - The round-trip message names the size this deployment can actually inline, so the model has a downscale target instead of a guess. - `python3: not found` from the sandbox reader was matching the generic "not found" in `isSandboxMissingFileError` and being reported as a missing image path, sending the model to `ls /mnt/data` while hiding a runner dependency. Image reads now classify only path-specific ENOENT forms as a missing file. * 📐 fix: Size a Truncated Read's Retry From the Cap It Revealed Addresses review round 3 on #15453. A ~1KB sandbox stdout cap is a real runner default, and the 1,536-byte narrowing floor sat above it: discovery stopped one step short and declared every image unreadable, even though small ones fit comfortably in the 576-byte window the configured path already derives. Narrowing now runs until no smaller window exists. It also no longer has to grope its way down: a truncated response emitted exactly as much as the runner allows, so its length measures the cap this deployment never declared, and the retry is derived from it directly — one round-trip instead of the eight blind halvings it takes to fall from ~49KB to ~1KB. Blind halving remains the fallback and bounds the result either way, so every retry is strictly smaller than the window that just failed. |
||
|
|
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 |
||
|
|
224512851a
|
🗂️ fix: Resolve Code Interpreter Destination Collisions (#15449)
* 🗂️ fix: Resolve Code Interpreter Destination Collisions
Codeapi mounts each `/exec` input at a destination derived from its `name`
and rejects the whole request when two entries land on one. LibreChat never
enforced that: `primeFiles` sent `name: file.filename` verbatim, and the
graph seed deduped by `(storage_session_id, id)` — identity, not destination.
Two conversation files sharing a filename therefore killed code execution for
the rest of the conversation. The rejected request never reaches the sandbox,
so no result comes back to collapse the pair, and every later turn re-primes
both and fails identically. Only code-generated outputs are covered by the
`(filename, conversationId, context, tenantId)` partial unique index; uploads
carry `context: message_attachment`, so one conversation can hold several
records sharing a filename — two uploads of `image.png`, or an upload whose
name a later execution wrote back in place.
`packages/api/src/files/code/destinations.ts` now owns the name → destination
mapping for every contributor to an `/exec` file list, mirroring codeapi's
conflict rule including directory-prefix conflicts:
- `primeFiles` claims per pushed file, disambiguating with a `-2`, `-3`
counter and echoing the resolved path into the tool context, so the model
is told where each file actually mounts. Claim order comes from immutable
`createdAt`, not `getFiles`'s `updatedAt` sort, which usage accounting and
re-upload both bump — order-dependent destinations would silently repoint
a path an earlier turn had already handed the model. Newest wins the bare
name, matching `ToolNode.updateCodeSession` and giving an in-place rewrite
precedence over the original it superseded.
- `seedCodeFilesIntoSessions` drops a later file whose destination is already
claimed. Renaming there would invent a path nothing told the model about,
and a conflict at that layer means one logical file arrived twice under
different storage pointers.
Fixes #15443
* 🧹 chore: Tolerate Holes in the Destination Priority Sort
`primeFiles` sorts the raw `getFiles` result before its own per-entry guard
runs, so the comparator has to survive a null row rather than throw ahead of
the loop that was already defending against one.
Also tightens the tool-context assertion in the upload-vs-output case, which
was passing on a trailing space rather than on the path.
* 🔁 fix: Address Codex Review on Destination Assignment
Four findings from the review of `
|
||
|
|
146664e0d1
|
🔐 fix: Redirect OAuth Browser Navigations Instead of Rendering JSON (#15440)
* 🔐 fix: Redirect OAuth Browser Navigations Instead of Rendering JSON The /oauth/* routes are reached by top-level browser navigation, so a middleware that rejects one with a JSON body makes the browser render that body as the page. Hitting the login rate limit during a social sign-in dropped the user on a blank page showing: {"message": "Too many login attempts, please try again after 5 minutes."} Mark those requests with markOAuthNavigation and have the two middlewares that can reject them answer with a redirect to the login page carrying an error code the client localizes. The login limiter stays a single instance so the attempt budget is still shared with POST /api/auth/login, and that route keeps its JSON 429 for the SPA. * 🧪 fix: Address PR Review Bot Findings chatgpt-codex-connector: - Split the inline `type TranslationKeys` specifier out of the value import, per the standalone `import type` convention in CLAUDE.md. This also clears the Static checks import-sort failure. - Cover the new OAuth error branches in Login.spec.tsx: each mapped `?error=` code raises its localized toast and the query is cleaned, and unmapped codes including prototype members raise nothing. |