Commit graph

4597 commits

Author SHA1 Message Date
Ravi Kumar L
50caff53d3 feat(langfuse): support media fanout gateway 2026-06-25 10:30:30 +02:00
Ravi Kumar L
ac9154ed55 fix(langfuse): harden tenant fanout config 2026-06-23 01:07:55 +02:00
Ravi Kumar L
59b9c8b242 fix(langfuse): type fanout collector config 2026-06-23 00:33:38 +02:00
Ravi Kumar L
25279f1d48 fix(langfuse): keep central fanout config collector-only 2026-06-23 00:28:24 +02:00
Ravi Kumar L
915c3f5f77 fix(langfuse): align score fanout toggles with traces 2026-06-22 23:54:10 +02:00
Ravi Kumar L
fcf2a89189 fix: align langfuse fanout env parsing 2026-06-22 10:57:34 +02:00
Ravi Kumar L
be4d874cef refactor: share string normalization helper 2026-06-22 10:41:19 +02:00
Ravi Kumar L
a2791bee5d fix: narrow remote agent run config 2026-06-22 10:38:34 +02:00
Ravi Kumar L
95023c8847 test: render fanout helm tests without dependencies 2026-06-21 23:55:23 +02:00
Ravi Kumar L
182c4a6636 feat: tune langfuse fanout batching 2026-06-21 23:54:06 +02:00
Ravi Kumar L
5e1c70eb58 test: guard langfuse fanout collector config drift 2026-06-21 23:41:57 +02:00
Ravi Kumar L
c50e1d282d fix: rename tenant fanout emergency toggle 2026-06-21 23:33:16 +02:00
Ravi Kumar L
b17579ae40 fix: treat blank tenant fanout toggle as disabled 2026-06-21 23:27:29 +02:00
Ravi Kumar L
6f36df4d12 style: sort agent imports 2026-06-21 23:24:58 +02:00
Ravi Kumar L
16d5fa63b3 test: remove unrelated suite stabilization 2026-06-21 23:21:45 +02:00
Ravi Kumar L
affd0194b2 docs: clarify langfuse compose destination scope 2026-06-21 21:54:53 +02:00
Ravi Kumar L
c939b11ee3 feat: route langfuse fanout by destination 2026-06-21 21:50:50 +02:00
Ravi Kumar L
5733145d4a style: format Langfuse fanout config 2026-06-21 20:14:54 +02:00
Ravi Kumar L
bd1bfdd485 test: cover central Langfuse env fallback 2026-06-21 20:04:46 +02:00
Ravi Kumar L
baf40da22a fix: gate tenant Langfuse fanout 2026-06-21 19:54:53 +02:00
Ravi Kumar L
093bdde4e3 fix: omit empty Langfuse tenant fields 2026-06-21 17:56:08 +02:00
Ravi Kumar L
0a0ce6a76c test: fix upload dialog import order 2026-06-21 17:48:58 +02:00
Ravi Kumar L
225d50bebe test: stabilize librechat suite 2026-06-21 17:41:43 +02:00
Ravi Kumar L
cef05dd443 fix: clarify Langfuse fanout collector config 2026-06-21 15:48:33 +02:00
Ravi Kumar L
ba588f1bb0 docs: prepare Langfuse fanout for OSS setup 2026-06-21 02:04:55 +02:00
Ravi Kumar L
24a90392f5 feat: fan out Langfuse feedback scores 2026-06-21 01:52:16 +02:00
Ravi Kumar L
45ec145029 feat: add opt-in Langfuse fanout collector 2026-06-21 00:40:48 +02:00
Pascal Garber
ef65f4a015
🪙 chore: drop redundant voxtral-small token pricing key (#13867)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
`voxtral` already matches the versioned id `voxtral-small-24b-2507` via the
longest-substring lookup, and both keys carry the same rate, so the separate
`voxtral-small` entry is redundant. Follow-up to #13863 per review note.
2026-06-20 17:24:21 -04:00
Danny Avila
c9180d1ad6
🎯 fix: Narrow Public Share 401 Bypass to the Share Endpoint Only (#12905)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
2026-06-20 13:57:21 -04:00
Danny Avila
f76a5faa9e
📌 feat: Seed Default Pinned Tools and MCP Dropdown via Interface Config (#13865)
*  feat: Add `defaultPinnedTools` interface config for default tool & MCP pinning

Adds an `interface.defaultPinnedTools` string array letting admins pin tools and the MCP servers dropdown to the prompt bar by default for all users.

- Tool keys (artifacts, execute_code, web_search, file_search, skills) pin their badge via `useToolToggle`.

- The keyword `'mcp'` or a configured MCP server name pins the MCP dropdown via `useMCPSelect`.

- Only seeds initial state; a user's stored pin preference always wins. When unset, tools start unpinned and the MCP dropdown keeps its legacy default (pinned).

Unifies the approaches from #11646 (pinnedTools) and #9251 (defaultPinMcp) into one config key.

* 🐛 fix: Apply defaultPinnedTools pin once startupConfig resolves

On a cold load, useToolToggle can mount before useGetStartupConfig() resolves, so defaultPinned starts false and useLocalStorageAlt eagerly persists it; its init effect never re-runs for the later config-driven default. Fresh users would then miss the admin-configured default pin whenever startup config was not already cached.

Capture whether a pin preference existed before mount (pre-seed) and, once startupConfig arrives, apply the real default for users with no prior preference. Runs once and never overrides an existing stored pin, so the conservative behavior for existing users is preserved.

* 🐛 fix: Preserve pin clicks made before startupConfig resolves

The cold-load default-seeding effect captured the stored-pin state only at mount, so a pin toggled before startupConfig resolved was treated as no-preference and overwritten when the admin default applied.

Track explicit pin toggles via a ref (set through the returned setter) and skip the default application when the user has interacted in-session — in addition to the existing stored-preference guard.
2026-06-20 13:40:10 -04:00
Serhii Zghama
8824e8f918
🚪 fix: Gate Artifacts Toggle on Agent Capability Flag (#13665)
* fix: hide artifacts toggle when capability is disabled

The artifacts badge ignored the agent capabilities config, so a pinned
toggle stayed visible after the artifacts capability was turned off.
Gate the component on artifactsEnabled via useAgentCapabilities, matching
how Skills, FileSearch and CodeInterpreter already handle their capability.

* style: fix import order in Artifacts.tsx

* style: Sort mutation type imports

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-06-20 11:52:36 -04:00
Pascal Garber
dfc1178031
🪙 feat: Add Token Pricing for Devstral, Voxtral, Holo2, and Mistral Medium (#13863)
These open-weight models are not in tokenValues, so they fall back to
defaultRate ($6/1M) for balance/transaction accounting on custom
OpenAI-compatible endpoints (e.g. Scaleway, where they are served).
Add representative per-1M-token USD rates:

- devstral        0.4 / 2.0   (Mistral API pricing)
- mistral-medium  1.5 / 7.5   (Mistral API pricing, Medium 3.5)
- voxtral(-small) 0.1 / 0.4   (Mistral API pricing, text)
- holo2           0.3 / 0.7   (Scaleway Generative APIs public pricing)

Generic keys are used so versioned ids (e.g. devstral-2-123b-instruct-2512,
mistral-medium-3.5-128b, voxtral-small-24b-2507, holo2-30b-a3b) match via the
existing longest-substring lookup.
2026-06-20 11:34:09 -04:00
Tómas Pálsson
229c54c843
🪢 fix: Paginate MCP tools/list to Load All Tools (#13840)
* 🪢 fix: Paginate MCP tools/list to load all tools

MCP `tools/list` is cursor-paginated, but LibreChat only ever read the
first page. `MCPConnection.fetchTools()` called `client.listTools()` once
and discarded `nextCursor`, and `MCPServerInspector` — which builds the
agent-facing tool registry at startup and per request — called the raw
`client.listTools()` directly. Servers that paginate (e.g. an aggregating
gateway exposing hundreds of tools) only ever exposed page one; tools on
later pages were never registered, and invoking one returned
"This tool's MCP server is temporarily unavailable."

- `MCPConnection.fetchTools()` now follows `nextCursor` across pages and
  concatenates every page's tools, bounded by a configurable page cap
  (`MCP_TOOLS_LIST_MAX_PAGES`, default 50) and a repeated-cursor guard so a
  misbehaving server cannot loop forever. Tools already fetched are returned
  if a later page fails, and the no-throw error contract is unchanged.
- `MCPServerInspector.getToolFunctions()` and `fetchServerCapabilities()`
  now route through `fetchTools()`, so the canonical startup and
  per-request tool registry is fully paginated too.

* style: Sort MCP test imports

* style: Sort mutation type imports

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-06-20 11:04:06 -04:00
Danny Avila
ff81377573
🚦 ci: Stop Auto-Indexing PR Branches in GitNexus Index (#13866) 2026-06-20 11:03:21 -04:00
Danny Avila
9dd0df9d61
🔑 feat: Surface User-Provided API Keys in Settings, Scoped to Reachable Endpoints (#13864)
Adds a "Provider API keys" entry under Settings → Data controls → API keys
that lists every endpoint requiring a user-provided credential and lets users
set or rotate its key via SetKeyDialog. This is always reachable, so keys can
be managed even when `interface.modelSelect` is hidden by `modelSpecs`.

The endpoint list is filtered the same way the mention popover and model
selector menu are:
- No modelSpecs → every user-provided endpoint.
- modelSpecs configured → limited to spec endpoints ∪ `modelSpecs.addedEndpoints`.
- agents reachable (with access) → expanded to the agents `allowedProviders`
  (all providers when unrestricted).

Reworks #13303 onto the registry-driven Settings dialog (#13722); the prior
standalone tab and the `APIKeys` directory are superseded (the latter also
collided with the agent `ApiKeys` feature from #13819).
2026-06-20 10:54:30 -04:00
Matheus Serpa
21d98b85bd
🏷️ fix: Scope File Search entity_id to Agent Knowledge-Base Files Only (#13693)
User-attached files are embedded by the RAG API under the user id (no
entity), while only agent knowledge-base files are embedded under the
agent's entity_id. Sending entity_id in every /query request made the
RAG API's entity filter return no results for user attachments — with a
shared agent, files attached to the message were effectively invisible
to the file_search tool, while knowledge-base files kept working (which
masked the bug).

primeFiles now tags each file with fromAgent (whether it belongs to the
agent's file_search.file_ids) and createQueryBody only includes
entity_id when fromAgent === true — the safe default for callers that
omit the flag is to query without entity scoping. Tests cover KB files,
user attachments, the omitted-flag default, and restore RAG_API_URL.
2026-06-20 10:18:25 -04:00
Airam Hernández Hernández
3926fda234
🎒 fix: Apply OCR Context to Responses API Agents and Handoffs (#13707) 2026-06-20 10:17:09 -04:00
Danny Avila
59637e136f
📦 chore: Bump @librechat/agents to v3.2.43 (#13854)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
2026-06-19 16:20:27 -04:00
Danny Avila
f8aa45d05e
🔚 feat: Add Bottom Terminus Node to Message Minimap Navigation (#13853)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
*  feat: Add scroll-to-bottom terminus node to MessageNav

Append the chat's bottom (#messages-end) as a terminal rib in the message
minimap so it is reachable by click, drag-scrub, and the down chevron like
any message. Rendered as a distinct centered dot rather than a line rib, and
gated on the #messages-end sentinel actually existing.

Also clamp each rib's snap target to the container's max scroll so the down
chevron no longer stays stuck enabled at the bottom (the terminus can never
scroll its top to the container top).

* 🐛 fix: Scope MessageNav terminus to its own scroll container

The terminus rib stored the shared constant id 'messages-end', which is
rendered once per MessagesView. With multiple navs mounted, the global
document.getElementById lookups resolved the first chat's sentinel, breaking
the per-instance isolation guaranteed by the existing multi-instance tests.

Resolve the terminus via the nav's own scrollableRef container
(querySelector), leaving the globally-unique message ids on the fast
getElementById path. Adds a multi-instance test covering the terminus.
2026-06-19 14:10:59 -04:00
Danny Avila
8969034ad1
fix: Strip remaining unsupported JSON Schema keywords for Gemini MCP tools (#13850)
*  fix: Strip remaining unsupported JSON Schema keywords for Gemini MCP tools

Gemini's FunctionDeclaration.parameters schema rejects more JSON Schema
keywords than sanitizeGeminiSchema previously stripped. MCP tools shipping
examples/readOnly/multipleOf/uniqueItems/prefixItems/etc. still 400 with
`Unknown name "<key>"`, the same class as #13623 (exclusiveMinimum).

Verified live against gemini-2.5-flash and gemini-3.5-flash: each added
keyword is rejected through `parameters`, and @langchain/google-genai only
removes additionalProperties/$schema, so they must be stripped here.

*  refactor: Make Gemini strip-list fully live-verified; preserve `default`

Probed every candidate keyword against both the live Gemini API
(gemini-2.5-flash, gemini-3.5-flash) and Vertex AI. Confirmed the inferred
siblings (dependencies/dependentSchemas/contentSchema) are rejected, so they
stay. Dropped `default`: it is part of Gemini's Schema and is accepted by both
the Gemini API and Vertex (no documented reason for its removal in #13623), so
it is now preserved instead of stripped.

*  fix: Preserve `default` data and synthesize array `items` (Codex P2s)

Addresses two Codex findings on the strip-list rework:

- `default` is now copied verbatim instead of recursed, so object/array default
  values (e.g. `{ id: 'abc', readOnly: true }`) keep ordinary data keys that the
  schema-recursion would otherwise strip.
- `prefixItems` is dropped but its first member is synthesized into `items`, since
  Gemini's API requires `items` on every array (live: itemless array => 400; the
  synthesized `{type:array, items:{...}}` => 200 on Gemini 2.5/3.5 and Vertex).

Third finding (patternProperties -> empty object) not actioned: live probing shows
`{type:'object'}` with no properties is accepted by both the Gemini API and Vertex.

*  fix: Treat boolean/tuple array `items` as missing (Codex P2)

The Draft 2020 tuple form `prefixItems: [...], items: false` slipped through: the
`'items' in collapsed` check treated boolean `false` as a real item schema, so no
fallback was synthesized and `items: false` was emitted — which Gemini rejects
(live: `items: false`/`true` => 400 "Invalid value").

Now `items` is only kept when it is a schema object; boolean and tuple-array
(`items: [...]`) forms are dropped, a `prefixItems` member is synthesized when
present, and any array still missing `items` falls back to `{}` (verified accepted
by the Gemini API and Vertex). Adds an `isObjectSchema` guard + tests.
2026-06-19 13:14:47 -04:00
Danny Avila
36ae268620
🐛 fix: resolve dayjs plugin ESM imports in data-provider (#13851)
`parsers.ts` imported `dayjs/plugin/utc` and `dayjs/plugin/timezone`
without a file extension. The tsdown build externalizes all bare imports,
so it emitted those specifiers verbatim into `dist/index.mjs`. dayjs@1.11
ships no `exports` map, so under strict Node ESM the extensionless
subpaths fail with ERR_MODULE_NOT_FOUND ("Did you mean to import
'dayjs/plugin/utc.js'?"), breaking every strict-ESM consumer that
transitively imports data-provider (and data-schemas, which re-exports
it) — e.g. vitest suites in downstream apps.

Add the `.js` extension so the externalized imports resolve. With
moduleResolution: bundler the types still resolve from the plugin
`.d.ts`. Bump to 0.8.507 to supersede the broken 0.8.506 publish.

Verified: build clean, `dist/index.mjs` imports under strict Node ESM
(node --input-type=module), parsers specs 50/50.
2026-06-19 11:11:08 -04:00
Danny Avila
e6fc232ed2
🌍 i18n: Update translation.json with latest translations (#13836) 2026-06-19 10:42:15 -04:00
Danny Avila
91f25b8302
📦 chore: bump @librechat/agents to v3.2.42 (#13848)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 🔧 chore: Update dependencies in package-lock.json and package.json

Bump `form-data` to version 4.0.6 and update `hasown` and `mime-types` dependencies in package-lock.json. Add an `overrides` section in package.json to ensure compatibility with the new `form-data` version.

* 📦 chore: Bump `@librechat/agents` to v3.2.42
2026-06-19 09:47:46 -04:00
Danny Avila
2c6db6bf73
🪞 fix: Match Prompt Cache TTL Control to Region Combobox Styling (#13839)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
The Prompt Cache Duration control used `component: 'dropdown'`
(DynamicDropdown → SelectDropDown), which renders with a different
trigger style than the sibling Region selector and expands its options
inline (headless-ui Listbox) — causing a visual mismatch and layout
shift in the parameters panel.

Switch both the Anthropic and Bedrock promptCacheTtl definitions to
`component: 'combobox'` (DynamicCombobox → ControlCombobox), the same
component Region uses: matching rounded trigger styling, an Ariakit
popover (portal, no layout shift), and — as a bonus — it persists
selections via setOption on custom endpoints (DynamicDropdown's custom
branch is a no-op TODO). Renames the placeholder fields to the
combobox's selectPlaceholder/selectPlaceholderCode.
2026-06-18 22:25:54 -04:00
Danny Avila
268fcbb78d
🕐 feat: Add promptCacheTtl model parameter for 1h/5m cache duration (#13835)
Some checks failed
Publish `librechat-data-provider` to NPM / pack (push) Waiting to run
Publish `librechat-data-provider` to NPM / publish-npm (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
* 🕐 feat: Add promptCacheTtl model parameter for 1h/5m cache duration

Adds a user-configurable `promptCacheTtl` parameter (dropdown: 5m | 1h)
alongside the existing `promptCache` toggle for Anthropic, Bedrock, and
OpenRouter endpoints. Default is undefined so the agents SDK applies its
own default (1h), letting users opt down to the legacy 5m TTL.

- data-provider: schema, parameterSettings dropdown, types, bedrock picks
- data-schemas: convo/preset types + mongoose defaults
- api: thread promptCacheTtl into anthropic + openai(OpenRouter) llmConfig
- i18n: en translation keys for label/description/default placeholder
- tests: anthropic llm.spec coverage for set + unset cases

* 🔧 fix: Tie Bedrock promptCacheTtl to promptCache + thread OpenRouter TTL params (Codex review)

- bedrock.ts: clear promptCacheTtl whenever promptCache is off/unsupported,
  so an unsupported 1h is never sent on a non-caching Bedrock request
- openai/llm.ts: resolve promptCacheTtl through the same defaultParams/
  addParams/dropParams machinery as promptCache (via promptCacheTtlValue)
  so OpenRouter custom endpoints can configure/override/drop it
- tests: bedrock TTL-tied-to-promptCache cases; OpenRouter TTL default/add/drop

* 🎨 style: Sort imports in openai/llm.spec.ts (CI sort-imports)

*  test: Prove OpenRouter TTL-only selection honors promptCache default (Codex review)

OPENROUTER_DEFAULT_PARAMS injects promptCache:true into defaultParams, so a
TTL-only dropdown selection (promptCacheTtl set, promptCache switch untouched)
still resolves caching on and forwards the TTL. Add regression tests via the
real getOpenAIConfig entry point: TTL-only -> promptCache+TTL both set;
explicit promptCache:false -> both dropped.

* 🔖 chore: Bump librechat-data-provider to 0.8.506

* 🔧 fix: Drop Anthropic promptCacheTtl when promptCache is dropped (Codex review)

dropParams: ['promptCache'] deleted requestOptions.promptCache but left
promptCacheTtl behind, so the admin opt-out path could still carry a TTL
on a request with caching disabled. Clear the TTL alongside promptCache.
2026-06-18 16:36:43 -04:00
Dustin Healy
6a63531eb4
📒 feat: Audit Log Backend for SystemGrant Assign and Revoke Events (#13087)
* 🛡️ feat: Audit log backend for SystemGrants changes

Add an AuditLog Mongoose collection that records every grant assign/revoke as an append-only entry capturing the actor, target principal, capability, timestamp, and tenant scope. Wire the entry-write into the existing admin assignGrant and revokeGrant handlers so the admin panel's audit-log tab populates as grants happen.

The data-schemas package gains the IAuditLog type, a Mongoose schema with tenant + target compound indexes for keyset pagination, a model factory wired through createModels, and an AuditLog methods factory exposing recordAuditEntry, listAuditLogPage (cursor-paginated, faceted, search-aware), findAuditLogEntry, and streamAuditLogEntries.

The packages/api admin layer adds createAdminAuditLogHandlers with three handlers backing the routes the admin panel already consumes: GET /api/admin/audit-log returns paginated entries, GET /api/admin/audit-log/:id returns a single entry for the permalink drawer, and GET /api/admin/audit-log/export.csv streams CSV with formula-injection defang plus UTF-8 BOM.

The Express layer mounts the new router at /api/admin/audit-log behind requireJwtAuth and the ACCESS_ADMIN capability, matching the existing admin route pattern. The audit emission failure is logged via logger.error but never rolls back the grant.

* 🧹 chore: Audit log backend cleanup — offset pagination, name-based filters, type tightening

Switch listAuditLogPage from cursor-based to offset-based pagination with skip().limit() + parallel countDocuments, returning { entries, total } instead of { entries, nextCursor }; the cursor encode and decode helpers are no longer needed and have been removed.

Interpret the actorId and targetPrincipalId filter parameters as case-insensitive partial regex against the denormalized actorName and targetName fields rather than exact-match against the underlying ObjectId. Admin panel users naturally filter by human name, not by Mongo identifier.

Replace the broad Record<string, unknown> casts on req.query with a typed AuditLogQuery shape, drop two unused exported types and the now-unused mongoose Types import, and fix the streamAuditLogEntries Omit literal to match the interface and the offset-based design.

* 🛠️ fix: Address audit log review feedback (CI typecheck, ISO offsets, no-op revoke, deps surface, schema, backpressure, tests)

Resolve the duplicate AuditAction export that broke the data-schemas TypeScript check by importing the canonical declaration from types/admin instead of re-declaring it in types/auditLog.

Accept timezone-offset ISO 8601 timestamps such as 2026-05-01T09:30:00+02:00 in the from and to filter params and reject local-time strings without a zone so every request resolves to an unambiguous instant.

Skip the audit emission on no-op revokes: revokeCapability now returns deletedCount so the admin handler can omit the grant_removed entry when the target grant did not exist, keeping the audit trail factually accurate. Mocks in the existing grants.spec.ts updated to the new return shape.

Drop the required recordAuditEntry from AdminAuditLogDeps since the audit-log handler factory never consumes it; the grants handler factory keeps its optional dep for the write path.

Tighten the tenantId validator on the audit log schema to require a non-empty trimmed string, and rewrite the listing-index comment to describe deterministic offset sort instead of keyset pagination.

Stream the CSV export with explicit backpressure (await drain when res.write returns false) and abort on client disconnect so a cancelled download no longer pins a Mongo cursor or buffers unbounded data in memory.

Add packages/data-schemas/src/methods/auditLog.spec.ts covering tenant and platform scoping, single and multi action filtering, partial-name filtering for actor and capability, the createdAt window, offset pagination with total, ObjectId and date stringification on the wire, regex-metacharacter escape, and streaming completeness.

* 🛠️ fix: Address P1 audit-log review findings (cursor cancel, drain race, filter naming, type dedupe, tenant scope, log enrichment)

The CSV stream handler kept draining Mongo batches after the client
disconnected because the `for await` loop only honored its abort flag
inside `onEntry`. Thread an `isCancelled` callback into
`streamAuditLogEntries` so the methods layer closes the cursor as soon
as the handler sees `close`/`aborted`; a `finally` block guarantees
release on throw. The drain promise in `writeChunk` now races against
the response's `close` event so a destroyed socket cannot strand the
handler on a `drain` that will never fire.

The HTTP filter keys `actorId` and `targetPrincipalId` always did
case-insensitive substring matches on the denormalized `actorName` /
`targetName` columns, never on ObjectIds — a client passing a real id
silently got zero rows. Renamed the wire-level keys to `actorQuery` /
`targetQuery` (matching what the matcher actually does) and kept the
old names as deprecated aliases for one release so the sibling
admin-panel PR can migrate without breaking; each legacy use logs a
deprecation warning. Renamed the corresponding fields in
`AuditLogFilters` too.

`AdminAuditLogEntryWire` duplicated `AdminAuditLogEntry` from
`types/admin.ts` field-for-field, violating the no-duplicate-types
rule. Deleted the duplicate, hoisted `AuditLogPage`,
`RecordAuditEntryInput`, and `AuditLogFilters` from
`methods/auditLog.ts` into `types/auditLog.ts`, and updated the
handler, method factory, and re-exports accordingly.

`tenantFilter` treated `''` as a valid tenant scope, producing a
`{ tenantId: '' }` query that silently returned nothing while the
schema validator rejected `''` on writes. Switched to a strict
`typeof tenantId === 'string' && tenantId.trim().length > 0` check so
reads agree with writes, with new spec coverage for empty and
whitespace-only inputs.

Audit-write failures now log the full forensic payload (action,
capability, tenantId, actorId, target metadata) inside a single meta
object so winston's standard signature surfaces it correctly; a comment
on the catch block explains why the failure mode stays silent (it must
never block a privileged operation).

Stronger filter parsing: invalid `action` values and unknown
`targetPrincipalType` now return 400 instead of silently dropping.
Extracted `MAX_LIMIT` to a constant. Replaced the
`Record<string, Date>` cast in `buildFilter` with a typed local.
Switched the stream cursor to `lean<IAuditLog[]>()` and removed the
`as IAuditLog` cast inside the loop.

*  test: Cover admin audit-log handler with unit tests for auth, validation, tenant isolation, CSV output, and abort

The sibling admin handlers (grants, groups, roles, users) all have
handler specs; this one was missing. The new suite covers 401 on a
missing `req.user`, 400 on malformed ISO `from` / `to`, 400 on
limit > 500, 400 on negative offset, 400 on an unknown action or
`targetPrincipalType`, 400 on a non-ObjectId `:id`, 404 when the
methods layer returns null, that the caller's `tenantId` (not a
forged query-string `tenantId`) is the one passed to the methods
layer, that `actorQuery` / `targetQuery` round-trip, that the
deprecated `actorId` / `targetPrincipalId` aliases still map through,
that the CSV stream emits the BOM as the first chunk with CRLF line
endings and the expected header labels, that quotes, commas, and
newlines are properly escaped, that the formula-injection prefixes
(`=` `+` `-` `@` tab CR) are defanged, that an `isCancelled` callback
reaches the methods layer and flips to true on client `close`, and
that `res.end` is skipped when the client disconnected mid-stream.

* 🛡️ feat: Enforce append-only AuditLog at the schema level

Every field is now marked `immutable: true`, and pre-hooks on the
schema reject `updateOne`, `updateMany`, `findOneAndUpdate`,
`findOneAndReplace`, `replaceOne`, `deleteOne`, `deleteMany`,
`findOneAndDelete`, plus any `save()` against an existing document.
`timestamps` is reduced to `{ createdAt: true, updatedAt: false }`
since a mutable timestamp would imply mutation is allowed, and
`updatedAt` is dropped from `AuditLog` / `IAuditLog`. The methods
spec resets state between tests via the raw driver (`AuditLog.collection.deleteMany`),
which bypasses the pre-hooks; new specs assert that the model-level
update / delete / re-save paths reject with the append-only error and
that `updatedAt` is not stamped on new documents.

* ♻️ refactor: Share MAX_AUDIT_LOG_LIMIT between methods and handler

Renamed the methods-layer constant from the generic `MAX_LIMIT` to
`MAX_AUDIT_LOG_LIMIT`, exported it through `@librechat/data-schemas`,
and consumed it from the handler instead of duplicating `500` there.
Now the limit is single-sourced; bumping it once updates both the
clamp inside `listAuditLogPage` and the 400-error boundary the
handler returns to clients.

* 🛡️ feat: Gate audit-log routes on a dedicated `READ_AUDIT_LOG` capability

The audit-log routes were gated on `ACCESS_ADMIN`, which conflates "can log
into the admin panel" with "can see who granted what to whom." Anyone with
`ACCESS_ADMIN + READ_CONFIGS` (a config reviewer with no people-management
authority) could read the grant history of every user, group, and role —
information they have no need to know.

`READ_AUDIT_LOG` ('read:audit_log') is now an explicit, separately grantable
read capability with no MANAGE counterpart, matching the append-only nature
of the collection. `seedSystemGrants` iterates `Object.values(SystemCapabilities)`
so existing ADMIN-role seeds pick it up automatically on next startup.

This also makes an "auditor" persona possible: hold `ACCESS_ADMIN + READ_AUDIT_LOG`
without any MANAGE_* grants and you can review history without modifying anything.

* ♻️ refactor: Share AUDIT_ACTIONS, tighten audit dep types, document route order

Exports a runtime AUDIT_ACTIONS array from packages/data-schemas alongside the
AuditAction type so the Mongoose schema enum and the HTTP handler's whitelist
consume one source of truth instead of duplicating the literal pair.

Switches the grants handler's recordAuditEntry dep typing from a duplicated
inline object literal returning Promise<unknown> to the published
RecordAuditEntryInput type returning Promise<void>, and tightens the local
emitAudit args to AuditAction. Replaces the local ParsedFilters interface in
the audit-log handler with Omit<AuditLogFilters, 'offset' | 'limit'> to drop
the duplicate definition.

Drops the optional marker on AuditLog.createdAt. Mongoose always sets it at
insert time, so callers treating it as nullable were guarding against a state
the schema does not produce.

Adds a comment on api/server/routes/admin/audit.js noting that /export.csv
must precede /:id so a future contributor does not accidentally reorder them
into a 404 trap.

* 🛡️ feat: Resolve audit names without extra DB round-trips

For the actor name, JWT-authenticated `req.user` already carries `name`,
`username`, and `email`. `resolveUser` now derives the actor display name
from `req.user` directly and threads it through the caller context, so
every grant assign and revoke no longer triggers a separate `getUserById`
lookup.

For the target name, replaces the previous always-store-the-principalId
behavior (which buried opaque ObjectId strings in immutable audit rows
for USER and GROUP targets) with a `resolveTargetName` dep. ROLE
principals continue to use `principalId` directly because the SystemGrant
model stores role names there. USER and GROUP principals route through
the new dep, which in `api/server/routes/admin/grants.js` calls
`db.getUserById` or `db.findGroupById` respectively and falls back to
the principalId on miss or error so the audit row stays intelligible.

Drops the misleading "display name lookup happens in a later iteration"
comment.

*  test: Cover audit emission, scope emitAudit to today's ROLE-only surface

Fixes a misleading test that claimed to verify "idempotent even if the grant
does not exist" while mocking deletedCount: 1 (the grant DID exist). Replaces
it with the actual no-op scenario (deletedCount: 0) and adds an assertion
that recordAuditEntry is NOT called, since the whole point of the
deletedCount > 0 gate is to avoid fictitious revocation rows.

Adds a dedicated audit emission describe block covering: grant_assigned
emission with the actor name resolved from req.user, grant_removed
emission when deletedCount is positive, and the no-emission fallback when
recordAuditEntry is not configured. The actor-name assertions exercise the
name / username / email fallback chain in resolveUser.

The previous commit also added a `resolveTargetName` dep and an
emitAudit branch for USER/GROUP targets. The grants surface is ROLE-only
today (MANAGE_CAPABILITY_BY_TYPE has only PrincipalType.ROLE), so that
code path is unreachable from the handler. Removed the dep and the
branch; the audit row uses principalId as the target name, which is the
human-readable role name for ROLE principals. A comment in emitAudit
flags where to plumb resolveTargetName back in once USER and GROUP
grants are enabled.

* 🛠️ fix: Inclusive `to` date filter and reject inverted ranges

A `?to=2025-01-15` filter previously stopped at midnight UTC of that
day, silently excluding everything that happened on January 15. The
`parseIsoDate` helper now widens a bare `YYYY-MM-DD` to 23:59:59.999Z
when called with the `end` boundary. Full ISO timestamps are honored
exactly, so callers that want minute-precision can still get it.

Also rejects inverted ranges (`from` later than `to`) with a 400 so
operators see a clear error instead of a silent empty result.

* 🛡️ feat: Cap audit-log CSV exports at 100k rows; cover stream error path

Introduces MAX_AUDIT_EXPORT_ROWS (100k) and threads a `maxRows` option
through streamAuditLogEntries. The handler now passes the cap into the
stream so a careless admin script or a hostile auditor cannot pin a
Node worker and a Mongo cursor by exporting unbounded result sets.
Beyond 100k rows, callers should slice exports by from / to date.

Adds a methods-layer spec for the cap behavior, a handler-layer spec
that asserts the option is plumbed through, and a handler-layer spec
that exercises the streamAuditLogEntries-throws-after-headers-sent path
(catch block falls through to res.end instead of attempting JSON).

Documents on buildFilter that case-insensitive substring regex filters
(actorName, targetName, capability, search) cannot use a B-tree index
and degrade to a tenant-scoped partition scan, so deployments with
hundreds of thousands of audit rows per tenant should constrain those
queries with a date window.

* 🧹 chore: Spell CSV_BOM as  and drop a gratuitous optional chain

`revokeCapability` is typed `Promise<{ deletedCount: number }>` so the
`?.` on `revokeResult?.deletedCount` only obscured that the value cannot
be nullish.

`CSV_BOM` was a literal U+FEFF character invisible in most editors. Now
spelled as the Unicode escape so readers can see the constant; the test
that asserts on the first emitted chunk uses the same escape.

* 🔧 chore: Allowlist AuditLog in the tenant-isolation coverage guard

The AuditLog collection carries a tenantId field but scopes tenancy manually
inside listAuditLogPage / streamAuditLogEntries / recordAuditEntry using the
same $exists: false convention as SystemGrant. The tenant-isolation plugin
coverage spec now allows that and asserts it stays accurate.

* 🛠️ fix: Normalize blank tenantId before persisting audit entries

The `recordAuditEntry` write path was treating any non-null tenantId as a
real string, so empty or whitespace-only values reached the schema validator,
failed the non-empty-string check, and silently dropped the audit row. The
read-side `tenantFilter` already treats those values as platform-level scope,
so the write path now mirrors it: blank or whitespace-only tenantId becomes
an omitted field, which matches `{ tenantId: { $exists: false } }` queries
and clears validation. Added a regression test that records two entries with
blank and whitespace tenantId and asserts both persist with the tenantId
field absent.

* 🎨 style: collapse expect.objectContaining onto one line to satisfy prettier

* 🔒 fix: block document-level deleteOne/updateOne on AuditLog

Mongoose registers deleteOne and updateOne pre-hooks as query middleware
by default. The query-level append-only block on AuditLog therefore did
not cover Document.prototype.deleteOne() or Document.prototype.updateOne(),
leaving a path where a caller that had already loaded an audit row via
findOne could call .deleteOne() or .updateOne() on the instance and bypass
the schema contract.

Explicit { document: true, query: false } registrations close the holes,
and the spec now covers both code paths against a real in-memory Mongo.

* 🔒 fix: require ACCESS_ADMIN on audit-log routes

Every other admin router (config, grants, users, roles, groups, auth)
enforces requireJwtAuth followed by requireCapability(ACCESS_ADMIN) before
any feature-specific capability check. The audit-log router only required
READ_AUDIT_LOG, which is independent of ACCESS_ADMIN in CapabilityImplications,
so a role delegated only READ_AUDIT_LOG without ACCESS_ADMIN could read or
CSV-export the audit trail and bypass the admin boundary.

Aligned the middleware chain with the rest of the admin surface so
ACCESS_ADMIN gates entry and READ_AUDIT_LOG gates the feature within it.

* 🎨 chore: re-sort imports after dev rebase

Post-rebase sort-imports against the merge target — six audit-log files
landed with stale import ordering relative to the current scripts/sort-imports.mts
rules on dev. CI's import-order job flagged the drift; running the script
locally rewrites them in place. No semantic changes.

* 🔧 fix: explicit type annotations on audit-log model + schema exports

Dev migrated packages/data-schemas builds from rollup to tsdown with
--isolatedDeclarations enabled, which requires every exported function to
declare its return type and every exported variable to declare its type.
Two of our audit-log exports got swept up:

  TS9007 models/auditLog.ts:12  createAuditLogModel return type
  TS9010 schema/auditLog.ts:12  auditLogSchema variable type

Added Model<t.IAuditLog> on the factory and Schema<IAuditLog> on the
schema variable, matching the sibling SystemGrant convention. No runtime
behavior change.

* 🔧 fix: align revokeCapability type annotation with implementation

The rebase auto-merge of systemGrant.ts kept dev's outer type annotation
(`revokeCapability: ... => Promise<void>`) but our implementation returns
`Promise<{ deletedCount: number }>` (added during the bot-review loop to
let the audit emitter distinguish a real revoke from a no-op against a
nonexistent grant). The mismatch surfaced as TS2719 on the methods record
return at line 520. Updated the type annotation to match the impl.

The caller at packages/api/src/admin/grants.ts:444 reads
`revokeResult.deletedCount` to gate the audit emit, so the wider return
type is what the rest of the code already assumes.

* 🔧 fix: explicit factory return type on createAdminAuditLogHandlers

Same tsdown --isolatedDeclarations migration that hit packages/data-schemas
also applies to packages/api; the audit-log handler factory's inferred
return type tripped TS9013 against the new build pipeline. Annotated the
factory with explicit handler signatures matching the sibling
createAdminGrantsHandlers convention. Used Promise<Response | void> for
the export handler because its final res.end() path returns undefined,
unlike the other two handlers which always return a Response.

* 🛡️ feat: Generalize audit log into a tamper-evident, extensible event substrate

Reworks the SystemGrant-only audit log into a general-purpose, append-only
compliance substrate designed to absorb future event classes (agent runs,
tool/MCP calls, config + permission changes, approvals) without reshaping the
record. Nothing was shipped yet, so this replaces the grant-specific wire
shape rather than layering aliases.

Schema / record shape (packages/data-schemas):
- schemaVersion + two-level taxonomy: category + namespaced action
  (grant.assigned/grant.removed), first-class outcome and severity.
- Structured actor{type,id,name} supporting non-user actors (system, agent,
  service, schedule, webhook, api); generic target{type,id,name}; open
  metadata map; request context{requestId,ip,userAgent,sessionId}.

Tamper-evidence (hash chain):
- Per-tenant chain keyed by chainKey with seq/prevHash/hash. Appends link to
  the previous hash; a unique {chainKey,seq} index serializes concurrent
  writes (dup-key retry) so the chain can never fork. createdAt is explicit so
  it's covered by the hash.
- verifyAuditChain() walks a chain and detects modification, deletion, and
  forged links; exposed via GET /api/admin/audit-log/verify.

Other best-practice gaps from the review:
- Keyset (cursor) pagination over seq alongside offset; stable under
  concurrent appends. nextCursor in the page payload.
- Retention: purgeAuditLogEntries() privileged prefix-purge with a confirm
  latch, returns a checkpoint; verify tolerates a purged prefix.
- Fail-closed option (AUDIT_LOG_FAIL_CLOSED) so a failed audit write can fail
  the grant request instead of being swallowed; default stays fail-open.
- Grant handlers now capture request context and emit the new shape.

CSV export updated for the new columns (incl. seq/hash). data-schemas bumped
to 0.0.54 for the sibling admin-panel consumer. Tests rewritten: 28
methods-layer cases (chain genesis/linking, tamper detection, keyset, purge)
and the handler/grants specs updated for the new shape, fail-closed, and the
verify endpoint.

* 🛠️ fix: Address Codex review on the audit-log substrate

- F1 (fail-closed atomicity): assign/revoke now compensate (rollback grant /
  restore grant) when a fail-closed audit write fails, so a 5xx never leaves an
  unaudited mutation.
- F5: only emit grant.assigned for a real change — skip the audit when the role
  already holds the capability (idempotent re-assert).
- F7: verifyAuditChain no longer silently trusts a non-genesis start; a purged
  prefix must be authorized by a trusted checkpoint (purge now returns
  {throughSeq, prevHash}), else verification fails as tampering.
- F4: block Model.bulkWrite on AuditLog (would bypass the append-only middleware).
- F3: CSV export appends an explicit TRUNCATED marker + logs when the row cap is hit.
- F6: reject out-of-range date-only filters (2025-02-31) instead of normalizing.
- F2: regenerate package-lock.json for the 0.0.54 data-schemas bump.

Tests: +1 methods (bulkWrite) +2 verify (deleted-prefix / checkpoint mismatch),
updated purge test for checkpoint flow; +4 api (re-assert skip, assign/revoke
fail-closed rollback, date reject, CSV truncation marker).

* 🛠️ fix: Address Codex round-2 on the audit-log substrate

- R2-1/R2-5 (P1/P2): base the grant.assigned audit decision on the atomic
  upsert result. grantCapability now returns { grant, created } via
  includeResultMetadata; the handler audits only when created. Removes the racy
  pre-read, which also mis-handled inherited platform grants vs a new
  tenant-scoped insert and concurrent double-assign.
- R2-2 (P2): namespace tenant chain keys (tenant:<id>) so a tenant whose id is
  literally the platform sentinel can't share the platform audit chain.
- R2-4 (P2): validate literal calendar tokens for full ISO timestamps too, so
  2025-02-31T00:00:00Z is rejected instead of normalizing to March 3.

Tests updated for the grantCapability { grant, created } contract (systemGrant +
grants specs) and the namespaced chain key (auditChainKey helper); +1 api date
case. data-schemas 141, api grants/audit 107 green.

R2-3 (deprecated actorId/targetPrincipalId aliases): not reinstating — the
surface is pre-release and its only consumer (admin-panel PR) migrates to the new
shape in lockstep, so there are no legacy clients to support.
R2-6 (role-deletion cascade emits no grant.removed): valid but a separate
workflow in roles.ts; tracked as a follow-up to keep this PR scoped.

* 🛠️ fix: Address Codex round-3 on the audit-log substrate

- R3-3 (P2): make a grant re-assert a true no-op — move grantedAt/grantedBy to
  $setOnInsert so an existing grant is never silently mutated when the audit is
  skipped (created:false now means nothing changed). grantedAt/grantedBy record
  the original grant.
- R3-2 (P2): report CSV export truncation exactly. streamAuditLogEntries returns
  { count, truncated }; truncated is true only when rows existed beyond the cap,
  so an exact-cap export is no longer falsely marked truncated.
- R3-5 (P2): block AuditLog.insertMany (another bulk path that skips the save
  hook and could inject forged seq/prevHash/hash and poison the chain).

Tests: +insertMany rejection, +exact-cap vs truncated stream cases, +exact-cap
export-not-truncated handler case. ds 142, api 108 green.

R3-1 (deprecated query aliases) and R3-4 (role-deletion cascade audit) are
re-flags of R2-3/R2-6 — holding the prior decisions (pre-release surface; separate
roles.ts workflow tracked as a follow-up), pending maintainer direction.

* 🛡️ feat: Audit grant removals from the role-deletion cascade

Closes the forensic gap Codex flagged (R2-6/R3-4): deleting a role removed its
SystemGrants with no audit entries. `deleteGrantsForPrincipal` now returns the
removed grants, and the role-deletion handler emits a `grant.removed` audit entry
per removed grant (actor = caller, target = role, metadata.capability, request
context), matching the explicit revoke endpoint. Fail-open — the role is already
deleted, so a failed audit is logged, not propagated; sequential to keep the
per-tenant hash chain ordered.

Extracted `buildAuditContext` to admin/context.ts (shared by grants + roles).
Tests: role-deletion emits one entry per grant / none when no grants; ds 110,
api admin 202 green.

* 🛠️ fix: Address Codex round-4 on the audit-log substrate

- R4-1 (P2): don't silently drop an audit row under heavy append contention.
  recordAuditEntry now retries duplicate-key seq collisions up to 12× with
  jittered backoff (was 5, no backoff), so realistic bursts of parallel admin
  writes resolve; the failClosed escape still applies on true exhaustion.
- R4-3 (P2): purge a contiguous seq prefix, not a date range. createdAt is
  app-generated, so under multi-instance clock skew a later seq can carry an
  earlier timestamp; a raw date delete could remove an interior row and break
  verification. purgeAuditLogEntries now resolves the date to the first retained
  seq and deletes only strictly-lower seqs, keeping the remaining chain contiguous.

Tests: +clock-skew purge case (no gap created). ds auditLog 33 green.

R4-2 (role-deletion grant audit) is a re-flag of R2-6/R3-4, already implemented
in 15472127d6 (roles.ts emitGrantRemovals + route wiring + tests); the finding's
cited line numbers predate that commit.

* 🛠️ fix: Address Codex round-5 on the audit-log substrate

- R5-1 (P2): scope each cascade grant.removed entry to the removed grant's own
  tenant, not the caller's. A platform admin deleting a role can remove
  tenant-scoped grants; those removals now land in the affected tenant's chain.
- R5-2 (P2): only return a purge checkpoint when rows were actually deleted. A
  no-op confirmed purge no longer mints a trust boundary that could legitimize a
  prefix it didn't authorize.
- R5-3 (P2): ensure the unique { chainKey, seq } index exists before appending
  (memoized createIndexes), so serialization doesn't depend on a background build
  — closes a silent chain-fork window under MONGO_AUTO_INDEX=false or at startup.

Tests: +per-grant-tenant cascade audit, +no-op-purge-no-checkpoint,
+index-built-before-append. ds auditLog 35, api roles 95 green.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-06-18 15:42:33 -04:00
Dustin Healy
fa20003952
🛂 refactor: Accept Targeted assign:configs for Config Scope-Lifecycle Endpoints (#13773)
* 🔓 fix: Accept Targeted assign:configs for Config Scope-Lifecycle Endpoints

Three admin-config endpoints currently require broad manage:configs: PUT /:principalType/:principalId for empty-overrides scope creation, DELETE /:principalType/:principalId for scope removal, and PATCH /:principalType/:principalId/active for the active toggle. The capability model already defines assign:configs:user|group|role for delegated administrators and validates that shape in isValidCapability, but no handler accepts it, so a delegate granted assign:configs:role via /api/admin/grants cannot manage scope lifecycle for the principal type they were explicitly delegated.

This aligns the server-side auth with the documented capability surface. Every destructive lateral path stays behind broad manage:configs: operations against the base config principal (__base__), non-empty PUT payloads that $set the full overrides field, and DELETE or toggle on a document whose existing overrides are non-empty (which would erase or neutralize sections the caller could not author). The new hasCapability dep on AdminConfigDeps is optional with a false default, so external consumers continue to get pre-PR behavior until they wire the resolver.

* 🛡️ fix: Block Assign-Only Scope-Lifecycle When Existing Doc Has Tombstones

The existing-overrides guard introduced in the prior commit only checked overrides, but configs also carry tombstones (suppressed inherited field paths) which are iterated during cascade resolution. An assign-only caller could delete, toggle, or empty-upsert a doc whose overrides is empty but whose tombstones is non-empty, which would erase or neutralize suppressions on fields they could not author. Extends the guard at all three call sites to treat a non-empty tombstones array as destructive state.

* 🚨 fix: Log TOCTOU Race When Assign-Only Lifecycle Op Hits Non-Empty Doc

The empty-state guard for assign-only callers performs a read-then-write across two DB roundtrips, so a concurrent broad-manage write can land between the guard and the destructive op. Adds post-write detection on the delete and toggle handlers: when the destructive op returns a doc whose state was non-empty at write time, emit logger.warn with the caller id, principal, and observed-state counts so ops can detect the race and restore from audit logs.

A fully atomic fix would require extending deleteConfig, toggleConfigActive, and upsertConfig in packages/data-schemas/src/methods/config.ts to support compare-and-swap filters, which is a wider design change than this PR's auth scope. Empty-payload upsert is not covered because $set replaces overrides, so the post-write doc no longer reflects pre-write state.

* 🔒 fix: Atomic Empty-State Filter for Assign-Only Scope-Lifecycle Writes

Replaces the read-then-check guard with an atomic Mongo filter on the destructive write itself. Adds an options.expectEmpty parameter to deleteConfig, toggleConfigActive, and upsertConfig in the shared data-schemas layer. When set, the filter requires both overrides and tombstones to be empty before the write matches. The TOCTOU race window is eliminated: a concurrent write cannot land between the empty-state check and the destructive op because they are now a single atomic operation.

For upsertConfig, the E11000 retry path returns null instead of falling back to a filterless update when expectEmpty is set, preserving the atomic property. Handlers fall back to findConfigByPrincipal only to disambiguate the null return between 404 (doc absent) and 403 (doc exists with non-empty state). The post-write logger.warn race detection added in the prior commit is removed as unreachable.
2026-06-18 15:40:58 -04:00
Dustin Healy
84886c56fb
🧷 fix: Preserve Document Priority on Section-Scoped Config Writes (#13772)
The patch and tombstone admin-config handlers accept a priority field on the request body, which controls the position of the entire Config document in the precedence cascade. Today, that priority is written unconditionally, even when the caller holds only a section-scoped grant such as manage:configs:memory. On a document containing overrides for other sections, this lets a section-scoped caller silently reorder overrides they have no permission to author.

This is a defense-in-depth fix for deployments using section-scoped grants. In a vanilla setup where admins hold broad manage:configs, the path is unreachable because the broad-capability short-circuit lets every priority change through, so the fix is a no-op for those callers. The change makes the contract safe-by-construction for any deployment that narrows the auth model, at no cost to upstream behavior.
2026-06-18 15:39:44 -04:00
Danny Avila
68d142d0e9
🦜 refactor: Use path for Read/Write/Edit/Create File Tools (#13834)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* fix(agents): use `path` for read/write/edit/create file tools

Pairs with @librechat/agents renaming the read_file/write_file/edit_file tool
parameter from `file_path` to `path` (models — esp. Kimi K2 — emit `path` far
more reliably, and it matches grep/glob/list_directory which already use `path`).

- tools.ts: LibreChat's own code/skill file-tool schemas use `path`
  (the skill read_file tool inherits the SDK definition, which is already renamed)
- handlers.ts: read `args.path` for the model-facing tool arg + error messages
- the internal host `readSandboxFile`/`writeSandboxFile` contract is unchanged
- tests updated

Requires @librechat/agents with the param rename (danny-avila/agents#250). All
agents unit suites green (175).

* chore: update @librechat/agents to v3.2.41 and bump related dependencies in package-lock.json and package.json files

* fix(api): Refactor header merging in MCPConnection to use Object.assign for clarity

* test(e2e): mock emits `path` for create/edit file-authoring tools

The mock LLM still sent `file_path` for the create_file/edit_file calls, which the
renamed handlers no longer read -> the skill-file-authoring e2e failed with
'Expected skill to be persisted'. Switch the fixture to `path` to match the tools.
(The internal readSandboxFile/writeSandboxFile contract stays on `file_path`, so
api/server/services/Files/Code/process.js and its spec are unchanged.)
2026-06-18 14:44:51 -04:00
Danny Avila
2fcba914f7
🔗 fix: Surface Share Permissions Load Error as Alert Button With Tooltip (#13833)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
2026-06-18 13:37:49 -04:00