Commit graph

4209 commits

Author SHA1 Message Date
Danny Avila
bc636c9a2e fix: harden Langfuse agent config handling 2026-05-12 00:10:20 -04:00
Danny Avila
0c2fdc92cd fix: encrypt Langfuse agent secrets 2026-05-11 22:10:03 -04:00
Danny Avila
dfc297ce2d fix: allow default Langfuse base URL 2026-05-11 21:47:38 -04:00
Danny Avila
0c809b7606 fix: preserve Langfuse tenant inheritance 2026-05-11 21:37:08 -04:00
Danny Avila
03e21214d0 feat: add tenant and agent Langfuse configuration 2026-05-11 21:20:53 -04:00
Danny Avila
3e7262cfe0
📦 chore: Bump @librechat/agents to v3.1.85 and mermaid to v11.15.0 (#13079)
* 📦 chore: Update @librechat/agents to version 3.1.85 in package-lock.json and package.json files

* 📦 chore: Update mermaid to version 11.15.0 in package.json and package-lock.json
2026-05-11 19:14:18 -04:00
Danny Avila
0a7255b234
🎭 feat: Support OpenID Audience On Refresh Grants (#13077) 2026-05-11 17:40:30 -04:00
Danny Avila
36e95353ed
🏷️ refactor: Rename Code Interpreter Labels To Run Code (#13071)
* fix: Rename Code Interpreter UI to Run Code

* fix: Remove unused Run Code i18n keys

* fix: Restore tool call labels

* fix: Keep assistant Code Interpreter copy

* fix: Update agent code environment copy

* fix: Update code environment upload copy

* fix: Use fresh run code locale keys

* fix: Update code environment test copy

* fix: Sort upload translation test keys
2026-05-11 16:24:33 -04:00
Danny Avila
c385f2ba88
📦 feat: Configure Skill Import Size Limit (#13073)
* fix: configure skill import size limit

* fix: validate skill import size in ui

* fix: align skill import size boundary

* fix: show exact skill import limit
2026-05-11 16:24:04 -04:00
Danny Avila
8735c1763c
🧵 fix: Preserve Upload Context Across Multipart Routes (#13072)
* fix skill multipart imports under strict isolation

* fix file upload context after multipart parsing

* fix skill upload tenant resolution

* fix rejected upload cleanup
2026-05-11 15:46:48 -04:00
Danny Avila
b32b328b87
🛡️ fix: Harden Artifact Routing Lookups (#13069) 2026-05-11 15:42:44 -04:00
Odrec
0f59cd79dd
🪙 fix: Pass appConfig to getBalanceConfig in set-balance script (#13070)
`config/set-balance.js` calls `getBalanceConfig()` without an argument,
so it cannot read the parsed `balance` section from librechat.yaml.
As a result the script always errors with "Balance is not enabled.
Use librechat.yaml to enable it" — even when balance is enabled.

`config/add-balance.js` already follows the correct pattern (fetch
`appConfig` via `getAppConfig()` and pass it into `getBalanceConfig`),
introduced in #9234. This applies the same pattern to set-balance.js.

Co-authored-by: Odrec <Odrec@users.noreply.github.com>
2026-05-11 15:08:23 -04:00
Danny Avila
0449c423a2
🗝️ fix: Enforce Skill Share Role Permission (#13062)
* fix: enforce skill share role permission

* fix: preserve share capability bypass

* refactor: move share policy middleware to api package

* style: order share middleware imports

* fix: satisfy share middleware type checks

* test: cover share policy resource types
2026-05-11 09:39:58 -04:00
Danny Avila
7631366f52
🪵 chore: Log Subagent Limit Hits (#13068) 2026-05-11 09:25:08 -04:00
Danny Avila
70b6bb69d3
🧬 fix: Bound Subagent Expansion (#13064)
* fix: Bound subagent expansion

* fix: Preserve subagent path depth
2026-05-11 08:53:53 -04:00
github-actions[bot]
508168fa57
🌍 i18n: Update translation.json with latest translations (#13058)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-11 08:41:16 -04:00
Danny Avila
9797c85e23
🧮 fix: Count Rejected Skill Import Bytes (#13063) 2026-05-11 08:40:31 -04:00
Danny Avila
822ad6c36a
🧯 fix: Bound Permission Superset Cache Inputs (#13065) 2026-05-11 08:39:37 -04:00
Danny Avila
52ccb1379b
🪪 refactor: Require Remote OIDC Audience for Agents API OAuth (#13066) 2026-05-11 08:38:13 -04:00
Danny Avila
7129b1b1e4
📜 refactor: Improve Skill Handling Logs (#13057)
* refactor: Streamline batch upload error handling in `uploadCodeEnvFile`
* refactor: Enhance session info error logging in `getSessionInfo`
* refactor: Update error logging to use `logAxiosError` in various agent handlers and skill file processing functions
* refactor: Consolidate missing resource checks in `createToolExecuteHandler` for better clarity
2026-05-11 02:15:51 -04:00
Danny Avila
763fab2e3e
🛂 fix: Restrict OpenID JWT Bearer Reuse (#13052)
* fix: Restrict OpenID JWT bearer reuse

* fix: Require signed OpenID reuse cookie

* fix: Bind OpenID reuse to authenticated user
2026-05-11 02:00:38 -04:00
Danny Avila
846eb0aa2c
🛰️ fix: Validate Vertex Endpoint Overrides (#13054)
* fix: Validate Vertex endpoint overrides

* fix: Allow Vertex PSC endpoint overrides

* fix: Allow restricted Vertex PSC endpoints
2026-05-11 01:11:14 -04:00
Danny Avila
5bab22d236
🛡️ fix: Gate Bash PTC Capabilities (#13053) 2026-05-10 21:23:02 -04:00
Danny Avila
030dc98a1d
☁️ fix: Enable Azure Agent Provider Uploads (#13045) 2026-05-10 17:47:05 -04:00
Danny Avila
1e9d0cbd0d
🖥️ style: Render Bash PTC Calls With Bash UI (#13046)
* fix: Render bash PTC calls with bash UI

* fix: Group bash execution tools consistently
2026-05-10 14:09:04 -04:00
Danny Avila
c3ec23f9b8
🌐 feat: Support Vertex AI Multi-Region Endpoints (#13044)
* feat: support Vertex AI multi-region endpoints

* fix: sync Vertex endpoint with final location
2026-05-10 13:41:58 -04:00
Danny Avila
8fc68ebac0
🧬 refactor: Align OpenRouter Reasoning Payloads (#13039)
* fix: Align OpenRouter reasoning payloads

* test: Update OpenRouter reasoning expectations

* fix: Preserve xhigh for future Claude models

* fix: Preserve OpenRouter Responses verbosity

* test: Type OpenRouter verbosity fixture

* fix: Preserve custom verbosity values
2026-05-09 21:04:21 -04:00
Danny Avila
715a4a5fc1
🧰 refactor: Use Bash PTC for Agent Tools (#13042)
* fix: Use Bash PTC for programmatic agent tools

* fix: Preserve legacy PTC event calls
2026-05-09 16:31:09 -04:00
Danny Avila
94ac44e3b4
🛡️ chore: Harden Docker Dev Image Builds (#13041)
* chore: harden Docker dev image builds

* chore: align Docker dev workflow timeouts
2026-05-09 16:30:28 -04:00
Danny Avila
2e683f112b
🦘 fix: Skip OpenAI Model Fetch For User-Provided Keys (#13038)
* fix: skip OpenAI model fetch if using user-provided key

There was a check present (via `opts.userProvidedOpenAI`), but it wasn't
working because `loadDefaultModels()` doesn't provide that parameter. As a
result, the server would repeatedly try to request models from OpenAI and get
401 errors in return.

We now check the env var directly, which matches how
`getAnthropicModels()` works.

* chore: remove unused OpenAI model option

* fix: honor explicit OpenAI key for model fetch

* fix: fall back from empty OpenAI option key

---------

Co-authored-by: Dan Lew <daniel@mightyacorn.com>
2026-05-09 16:12:25 -04:00
Danny Avila
80ce956c94
📜 fix: Scope Read File Prompt For Code Agents (#13040)
* fix: Scope read_file prompt for code agents

* fix: Align code read_file prompt behavior
2026-05-09 16:09:56 -04:00
Danny Avila
c67e2b54dc
🔐 feat: Mint Code API Auth Tokens (#13028)
* feat: Mint CodeAPI auth tokens

* style: Format CodeAPI download route

* fix: Prune CodeAPI token cache

* fix: Propagate CodeAPI managed auth

* test: Mock CodeAPI auth in traversal suite

* fix: Pass auth context to invoked skill cache

* feat: Mint CodeAPI plan context

* chore: Refresh CodeAPI auth guidance

* fix: Guard OpenID JWT fallback

* fix: Default CodeAPI JWT tenant in single-tenant mode

* chore: Update @librechat/agents to version 3.1.84 in package-lock.json and package.json files

* chore: Standardize references to Code API in comments and tests
2026-05-09 16:09:10 -04:00
Danny Avila
8a654dc8b1
🧭 feat: Add OpenRouter Prompt Cache Setting (#13029)
* feat: add OpenRouter prompt cache setting

* fix: type OpenRouter schema lookup

* fix: honor proxied OpenRouter prompt cache

* refactor: flatten endpoint schema fallback

* chore: Bump `@librechat/agents` to version 3.1.82

* fix: Default OpenRouter prompt cache params

* test: Align OpenRouter config expectations

* test: Update OpenRouter default cache expectation

* fix: Align OpenRouter Detection

* chore: Bump `@librechat/agents` to version 3.1.83

* docs: Remove OpenRouter prompt cache setup note

* refactor: Use provider enum for OpenRouter defaults

* style: Format OpenRouter defaults guard
2026-05-09 11:46:09 -04:00
Dustin Healy
0d5c2b339a
🛟 fix: Allow Empty modelSpecs.list to Unstick Admin-Panel Saves (#13036)
* 🛟 fix: Allow empty modelSpecs.list to unstick admin-panel saves

The unconditional `.min(1)` on `specsConfigSchema.list` rejected an empty
list even when `enforce: false`, leaving admin panels (which save fields
path-granularly) with no atomic way to clear the list once it had been
populated. Once an admin reached `list: [entry]` and deleted the only
entry, every subsequent save failed schema validation and the section
became stuck.

Relax the schema to `.default([])`. The `.min(1)` was added in #5218 as
part of bundled cleanup, not as a deliberate rule. Every consumer of
`modelSpecs.list` already handles the empty/undefined case (`?.list`,
`?? []`, length-checked), and `processModelSpecs` short-circuits to
`undefined` when the list is empty so the runtime treats it as "no
specs configured." No call site is load-bearing on length >= 1.

Tighten the `buildEndpointOption.js` enforce guard from
`?.list && ?.enforce` to `?.list?.length && ?.enforce`. Empty arrays
are truthy in JS, so the existing guard would have entered the enforce
branch on `list: []` and returned "No model spec selected" or "Invalid
model spec" had `processModelSpecs` ever been bypassed.

Add a runtime warn in `processModelSpecs` when `enforce: true` is
configured alongside an empty list, so operators see the resulting
"enforcement disabled" state in logs rather than silently getting a
permissive runtime.

Add coverage for the empty-list parse path in `config-schemas.spec.ts`
and for the empty-list-with-enforce branch in `buildEndpointOption.spec.js`.

* chore: update import order in config-schemas.spec.ts
2026-05-09 11:39:15 -04:00
Danny Avila
ac3600cdd7
🗂️ fix: Remove Generated Code Files From Prompt Context (#13037) 2026-05-09 11:38:53 -04:00
Danny Avila
c7a4e6d418
📦 chore: Bump @babel/preset-env to v7.29.5 (#13034) 2026-05-08 19:51:06 -04:00
Danny Avila
b922187abb
🛟 fix: Summarization Provider misses vertexai + case-mismatched custom endpoints (#13025)
`resolveSummarizationProvider` calls `getProviderConfig` to translate the
agent's resolved provider into an initializer + client overrides. Three
real-world inputs were unsupported and fell through to "raw provider"
fallback (silently dropping client overrides):

1. **`vertexai`** — not in `providerConfigMap` at all. Vertex shares
   initialization with Google (auth-only runtime distinction). Map
   `Providers.VERTEXAI` to `initializeGoogle`.

2. **`openrouter` (and other known custom providers) with CamelCase
   custom endpoint names** — agent main flow looks up endpoints
   case-sensitively (case-preserving keys are how
   `loadCustomEndpointsConfig` lets users have distinct entries
   differing only in case). Once it succeeds, `agent.provider` is
   normalized to lowercase. Downstream resolvers re-enter
   `getProviderConfig` with the lowercased value and miss configs
   whose `name` is camel-cased. Add a case-insensitive fallback,
   narrowly scoped to known custom providers and only after the
   case-sensitive direct lookup fails.

3. **Ambiguous case-insensitive matches (codex review feedback)** —
   if the user has e.g. `OpenRouter` and `OPENROUTER` (neither
   lowercase) and the agent runtime passes `openrouter`, the
   case-insensitive fallback could silently route to whichever entry
   appears first in the array (potentially different baseURL/apiKey).
   Detect multiple case-insensitive matches and throw a clear error
   with both names rather than picking arbitrarily.

## Tests

`providers.spec.ts` — new file, 7 tests:
- vertexai → Google initializer
- google (API key) → Google initializer (regression guard)
- case-insensitive fallback when only CamelCase entry exists
- exact-case match preserved when both casings exist (case identity)
- exact-case lowercase entry still resolves
- throws on ambiguous case-insensitive matches when no exact-case exists
- still throws when no match at all
2026-05-08 18:52:01 -04:00
Danny Avila
d90567204e
🛟 fix: persist Vertex Gemini 3 thoughtSignatures across DB round-trips (#13026)
When a tool round-trip is interrupted between the tool result and the
model's text reply (user aborted, network drop, pod restart, ...) and
LibreChat persists the partial assistant message, the next conversation
turn reconstructs an `AIMessage` from `formatAgentMessages` that has
`tool_calls` populated but no `additional_kwargs.signatures`. Vertex
Gemini 3 rejects the resumed request with 400 because the most recent
historical functionCall has no `thought_signature`.

## Storage shape

Capture as `Record<tool_call_id, signature>` rather than a flat array.
This addresses the codex P1 review:

  > When an assistant turn contains multiple sequential tool-call batches,
  > this restoration path writes all persisted thoughtSignatures onto only
  > the last tool-bearing AIMessage. Vertex/Gemini validates signatures
  > for each step in the current tool-calling turn, so earlier
  > functionCall steps reconstructed without their signature can still
  > fail with 400.

A single agent run can fire multiple `chat_model_end` events when the
loop cycles the LLM with intervening tool results — each cycle owns a
distinct `tool_call_id`. Per-id storage maps each signature back onto
the right reconstructed `AIMessage`, not just the last one.

## Mapping

`additional_kwargs.signatures` is a flat array indexed by *response part*
(text + functionCall interleaved). `tool_calls` is just the function
calls in their original order. Non-empty signatures correspond 1:1 with
tool_calls in order — see `partsToSignatures` in
`@langchain/google-common`. Single-pass walk maps `signatures[i]` (when
non-empty) onto the i-th `tool_call.id`.

## Pipeline

| Stage | File | Change |
|---|---|---|
| Capture | callbacks.js | `ModelEndHandler` accepts `Record<string,string>` map; walks signatures + tool_calls in tandem to record per-id. Gated on the map being provided — non-Vertex flows are no-op (and also no-op even when provided, since they don't emit signatures). |
| Plumbing | initialize.js | Allocate `collectedThoughtSignatures = {}`, share with handler + client. Always allocated; the JSDoc explicitly documents that it stays empty for non-Vertex providers. |
| Surface | client.js | `sendCompletion` returns `metadata.thoughtSignatures` when the map has entries; falls through unchanged when empty. |
| Persist | (existing BaseClient.handleRespCompletion) | Writes `metadata` from `sendCompletion` onto `responseMessage.metadata`. Mongoose `Mixed` — no migration. |
| Restore | formatMessages.js | Track every tool-bearing AIMessage produced from a TMessage. For each, build a position-aligned `additional_kwargs.signatures` array (empty placeholders for tool_calls without a stored sig). Agents' `fixThoughtSignatures` dispatches non-empty entries to functionCall parts in order. |

## Live verification

- **Single-step:** real Vertex `gemini-3.1-flash-lite-preview` resume-after-tool case. With fix  / without  400.
- **Multi-step (codex case):** real two-step agent loop (list /tmp → echo done). Each step's signature attaches to its own reconstructed AIMessage. With fix  / without  400.
- **Cross-provider:** Anthropic Claude haiku-4.5 + OpenAI gpt-5-mini accept the persisted/restored shape unchanged.

## Tests

`modelEndHandler.spec.js` (new) — 6 tests:
- maps non-empty signatures onto tool_call_ids in order
- accumulates per-id across multiple `model_end` events (multi-step)
- no-op when `collectedThoughtSignatures` is null
- no-op when `signatures` field missing (non-Vertex)
- no-op when `tool_calls` missing
- preserves existing `collectedUsage` array contract

`formatAgentMessages.spec.js` — 6 new tests:
- restores onto the AIMessage that owns the tool_call
- per-step attachment for multi-step turns (codex review case)
- preserves tool_call ordering when signatures are partial
- no-op when metadata.thoughtSignatures absent
- no-op when assistant has no tool_calls
- no-op when stored ids don't match any current tool_call

37 passing across 3 suites; 15 existing formatAgentMessages tests unchanged.

## Compatibility

- Backward-compatible — restore gated on `metadata.thoughtSignatures` being a populated object; capture gated on the map being provided.
- No schema migration — uses `Message.metadata: Mixed` already in place.
- Cross-provider safe — non-Vertex providers tolerate the field (verified live against Anthropic + OpenAI converters).
- Pairs with [agents#159](https://github.com/danny-avila/agents/pull/159) for full coverage on histories that mix plain-text and toolcall AIMessages.
2026-05-08 18:51:34 -04:00
Vinicius Dittgen
a565a61a23
⛴️ fix: Use Bitnami Legacy MongoDB Image in Helm Chart (#13032)
Bitnami moved versioned image tags from docker.io/bitnami to
docker.io/bitnamilegacy on 2025-08-28, which causes ImagePullBackOff
on a fresh Helm install of the LibreChat chart. Override the MongoDB
subchart image repository to bitnamilegacy/mongodb so installs
succeed out of the box.

Fixes #13031
2026-05-08 18:21:34 -04:00
Dustin Healy
e262219c8f
🔄 feat: Cross-Origin Admin OAuth Refresh (#13007)
* feat(admin-panel): add /api/admin/oauth/refresh endpoint for cross-origin BFF refresh

The cookie-based /api/auth/refresh controller can't be reached cross-origin
from a separately-hosted admin panel because the refresh-token cookie isn't
sent on cross-origin fetches. Add a dedicated POST /api/admin/oauth/refresh
endpoint that accepts the refresh token in the request body, exchanges it
at the IdP via openid-client refreshTokenGrant, and returns the same
response shape as /api/admin/oauth/exchange.

Implementation lives in packages/api/src/auth/refresh.ts as the
applyAdminRefresh helper. It validates the refreshed tokenset, looks up the
admin user by openidId (with optional user_id disambiguation when multiple
user docs share an openidId), mints the bearer via an injected mintToken
hook, and runs an optional onRefreshSuccess hook for downstream forks that
need to update server-side session state.

The default mintToken passed by the OSS route signs an HS256 LibreChat JWT
via generateToken so admin panel callers continue to use the existing local
JWT strategy. Forks that prefer to hand back an IdP-signed token (e.g. for
deployments where the JWT auth gate is JWKS-only) override mintToken
without changing the helper or the route.

Also threads expiresAt through AdminExchangeData and AdminExchangeResponse
so admin panel clients can drive proactive refresh before the bearer
expires. Defaults the OSS exchange flow to Date.now() + sessionExpiry.

* fix(admin-panel): address review feedback on /api/admin/oauth/refresh

mintToken now returns {token, expiresAt} so the minter is authoritative
for the bearer's lifetime instead of deriving it from the IdP `exp` claim.
The refresh response would otherwise lie to the admin panel and trigger
premature or late refresh cycles.

The helper now falls back to the inbound refresh_token when the IdP omits
one on rotation (Auth0 with rotation off, Microsoft personal accounts).
Without this the admin panel loses its refresh capability after one cycle.

Other hardening:

resolveAdminUser validates user_id with Types.ObjectId.isValid before
hitting Mongoose, avoiding a CastError that would surface as a generic
500 with no useful information for the client.

If user_id resolves to a user whose openidId does not match the refreshed
sub, throw USER_ID_MISMATCH (401) instead of silently swapping in a
different user matching the sub.

Wrap tokenset.claims() in readClaims so an IdP that returns a tokenset
without a usable id_token gets mapped to CLAIMS_INCOMPLETE (502) rather
than bubbling a raw exception.

findUsers now uses the same SAFE_USER_PROJECTION as getUserById so the
fallback path no longer pulls password/totpSecret/backupCodes into memory.

Removed dead fields (email on AdminRefreshClaims, id_token on
RefreshTokenset) and fixed import ordering per AGENTS.md.

Adds packages/api/src/auth/refresh.spec.ts: 18 tests covering the happy
path, userId disambiguation (match, invalid ObjectId, null, mismatch),
all error branches (IDP_INCOMPLETE, CLAIMS_INCOMPLETE for both throw and
missing sub, USER_NOT_FOUND, mintToken/onRefreshSuccess propagation), and
refresh-token preservation under rotation/no-rotation.

* chore(admin-panel): polish per re-review on /api/admin/oauth/refresh

readClaims now logs the original error name/message at warn before mapping
to CLAIMS_INCOMPLETE so a programming bug doesn't get silently rebadged
as an IdP problem in production logs.

The route handler's JSDoc now enumerates every error response (status +
error_code) so admin-panel implementors can plan for each branch without
reading the source.

Tightens the helper's surface: removed the now-dead `exp` field from
`AdminRefreshClaims` (only `sub` is read since the v2 mintToken refactor),
and tightened `AdminRefreshDeps.findUsers`'s projection parameter from
`string | null` to `string` so the contract matches actual usage.

Test polish: the userId-resolves-to-null fallthrough test now asserts the
exact `findUsers` and `getUserById` call arguments so a regression in the
fallthrough query shape is caught. The "skips onRefreshSuccess" test now
asserts a populated response shape rather than just `toBeDefined`.

Declined per prior triage and re-confirmed: a role guard inside
`applyAdminRefresh` (downstream `/api/admin/*` already enforces
ACCESS_ADMIN via requireCapability) and moving the IdP grant call out of
the JS route into TypeScript (matches existing oauth.js / openidStrategy
pattern; package-boundary refactor belongs in a separate PR).

* fix(admin-panel): reject /api/admin/oauth/refresh tokensets from foreign issuers

When the route handler can resolve the configured OpenID issuer, it now
threads it into applyAdminRefresh as expectedIssuer. The helper compares
that against the tokenset claims iss (after normalizeOpenIdIssuer on
both sides to absorb trailing-slash differences) and throws
ISSUER_MISMATCH (401) on mismatch.

The check is skipped when either side is unset so behavior is unchanged
for IdPs that don't return iss on a refresh-grant id_token, and for
older deployments where the OpenID config doesn't expose serverMetadata.

This is a defense-in-depth measure for the refresh path only. The
deeper OIDC posture fix (binding IUser lookup to (sub, iss) as a pair)
is pre-existing debt across openidStrategy.js and the regular exchange
flow as well, and belongs in a separate PR with the schema change and
backfill migration.

* fix(admin-panel): bind refresh user lookup to (sub, iss) and handle getOpenIdConfig throw

Two fixes raised on the PR thread that I previously misdescribed:

The user lookup in resolveAdminUser was keyed on openidId alone, so a
tokenset from a different issuer that happened to share the same sub
could resolve to a local user from a different IdP. Now exports
getIssuerBoundConditions and isUserIssuerAllowed from openid.ts (the
helpers findOpenIDUser already uses) and reuses them. The findUsers
filter becomes ($or of getIssuerBoundConditions for openidId) when an
expectedIssuer is provided, with the same legacy backward-compat
clause for users whose openidIssuer field was never populated. The
direct user_id path now also checks isUserIssuerAllowed and throws
USER_ID_MISMATCH if the stored openidIssuer disagrees with the
configured issuer.

The route's getOpenIdConfig() call was previously documented as
returning null when uninitialized; the actual implementation throws.
That made the if (!openIdConfig) guard unreachable, and an unconfigured
server would surface as 500 INTERNAL_ERROR rather than 503
OPENID_NOT_CONFIGURED. Wraps the call in try/catch so the documented
503 response is what callers actually receive.

Adds 4 tests covering the new lookup binding behavior.

* fix(admin-panel): re-check ACCESS_ADMIN on /api/admin/oauth/refresh

The IdP refresh token can outlive a capability/role change, so the
initial requireAdminAccess on the OAuth callback isn't sufficient.
Inject canAccessAdmin via the existing capability model
(hasCapability with SystemCapabilities.ACCESS_ADMIN, matching
requireAdminAccess so custom roles and user grants are honored)
and gate token minting on it. Capability backend errors are
warn-and-denied to keep the bearer-mint path fail-closed.

* fix(admin-panel): scope /api/admin/oauth/refresh to the request tenant

The same (openidId, openidIssuer) pair is allowed across tenants by
the user schema's unique index. The refresh helper was wrapping both
the direct getUserById and the fallback findUsers in runAsSystem,
bypassing tenant isolation, so an IdP identity that exists in two
tenants could resolve to the wrong tenant's user and mint a JWT
bound to that tenant.

Drop the runAsSystem wrappers, add a trusted tenantId option to
applyAdminRefresh, AND it into the fallback findUsers filter, and
assert it against the direct getUserById result. Mount
preAuthTenantMiddleware on the refresh route so the deployment's
X-Tenant-Id header drives the trusted tenant via ALS. Single-tenant
deploys (no header) keep the existing openidId-only behaviour.

Adds TENANT_MISMATCH (401) and a regression covering duplicate
(sub, iss) across tenants plus the direct-userId tenant assertion.

* fix(admin-panel): gate /api/admin/oauth/refresh on OPENID_REUSE_TOKENS

The OSS refreshController only refreshes OpenID tokensets when
OPENID_REUSE_TOKENS is enabled. The body-based admin variant was
unconditionally calling refreshTokenGrant, which made the flag
ineffective for the admin OAuth flow and let admin sessions keep
renewing in deployments that explicitly turned token reuse off.

Add the same isEnabled(process.env.OPENID_REUSE_TOKENS) check up
front and return 403 TOKEN_REUSE_DISABLED so the admin panel BFF
can surface the configuration mismatch instead of silently churning
through retries.
2026-05-08 17:23:02 -04:00
Danny Avila
22890771cf
🧭 fix: Preserve Resend Files for Subagents (#13030) 2026-05-08 17:21:52 -04:00
Danny Avila
a107520109
📦 chore: Bump @librechat/agents to v3.1.81 & npm audit fix (#13027)
* 📦 chore: Bump `@librechat/agents` to v3.1.81

* chore: npm audit fix
2026-05-08 16:20:03 -04:00
Danny Avila
3d5e5348a4
🧵 fix: Include Code Outputs in Thread File Lookup (#13023)
Code-execution outputs land on `messages.attachments` (set by
`processCodeOutput`), while user uploads land on `messages.files`.
The threadFileIds switch (#13004) walked only `files`, so on a
single linear thread:

  Turn 1: assistant produces sample.xlsx → attachment with codeEnvRef
  Turn 2: user says "add 2 rows"
          → primeCodeFiles: file_ids=0 resourceFiles=0
          → /exec sent files=[]
          → sandbox: FileNotFoundError: 'sample.xlsx'

The `getThreadData` walk found zero file_ids because the assistant's
codeEnvRef was on `attachments`, not `files`. Compounded by the
DB select string `'messageId parentMessageId files'` which didn't
pull `attachments` into memory in the first place — so even fixing
the walk in isolation wouldn't have surfaced them.

Both layers fixed:
  - `ThreadMessage` type adds `attachments?: Array<{ file_id?: string }>`
  - `getThreadData` walks both arrays, dedups via the same Set
  - `initialize.ts` selects `'messageId parentMessageId files attachments'`

## Test plan

`packages/api/src/utils/message.spec.ts` (+6 cases):
- collects file_ids from `attachments`
- walks both `files` and `attachments` on the same message
- regression: linear thread with code-output attachments across
  user→assistant→user→assistant produces the right file_ids
- dedupes shared ids that appear in both arrays
- skips attachments without file_id (mirrors `files` behavior)
- empty `attachments` array

`packages/api/src/agents/__tests__/initialize.test.ts` (+1 case):
- locks the DB select string includes `attachments` alongside
  `files` / `messageId` / `parentMessageId`

- [x] `npx jest src/utils/message.spec.ts` — 39/39 pass
- [x] `npx jest src/agents/__tests__/initialize.test.ts` — 33/33 pass
- [x] lint clean on all four touched files
2026-05-08 12:29:46 -04:00
Marco Beretta
26a6312917
🖼️ refactor: Tool Image Outputs outside of Tool Group Auto-Collapses (#12949)
* refactor(attachments): add variant prop to AttachmentGroup

* feat(tool-call): add hideImageAttachments prop to ToolCall

* fix(tool-call): keep MCP image outputs visible when tool group auto-collapses

* test(tool-call): verify MCP images hoist out of collapsed tool group

* fix(tool-call): hoist all grouped attachments and prevent ExecuteCode double-render

- rename hideImageAttachments -> hideAttachments and hide every attachment
  in the inner tool when a group auto-collapses, then hoist them via
  ToolCallGroup with default variant 'all' so non-image attachments survive
  the collapse alongside images
- thread hideAttachments to ExecuteCode so it skips its inline AttachmentGroup
  when grouped, preventing double-render when the group is expanded
- memoize sequentialParts and groupedParts in ContentParts (with
  groupAttachments rolled into each tool-group entry) so we don't re-flatMap
  on every render

* test(tool-call): cover hideAttachments contract and grouping integration

- ToolCall: assert AttachmentGroup is skipped when hideAttachments=true and
  rendered when explicitly false, locking the prop's contract
- ToolCallGroup: update variant assertion to 'all' (now hoists images and
  files together) and add a non-image-only hoist case
- ContentParts.integration: new test exercising the full
  ContentParts -> Part -> ToolCall -> AttachmentGroup chain with realistic
  MCP-shaped data (groups 2+ contiguous tool calls and hoists, single calls
  render inline, mixed image+file hoists, empty attachments are a no-op)

* fix(tool-call): extend hideAttachments to bash/read_file/skill/subagent

When the post-rebase dev branch added BashCall, ReadFileCall, SkillCall,
and SubagentCall as dedicated tool renderers, each rendered its own
inline AttachmentGroup. Once the parent tool group hoists every
attachment, those inline groups would double-render, so they now honor
the same hideAttachments contract as ToolCall and ExecuteCode.

Also seed the new ToolCallGroup mocks (Users icon, getToolDisplayLabel)
so the existing hoist test suite keeps passing on dev.

* fix(image-gen): suppress inline image when attachments are hoisted

OpenAIImageGen renders the generated image directly via <Image>. When
its tool_call lands inside a grouped tool call, the parent now hoists
those attachments into ToolCallGroup's AttachmentGroup, and the inline
<Image> would render the same file a second time. Thread hideAttachments
through Part -> ImageGen (agent-style branch) so the agent-style image
slot stays out of the way once the parent has hoisted.

* refactor(tool-call): drop dead variant prop and flatten render-part hooks

- AttachmentGroup's variant prop ('images' / 'non-images') had no callers
  after the final hoisting design landed, so remove the prop and the
  filtering branches; everything passes the default 'all' behavior.
- Replace the makeRenderPart factory + dual useMemo with two plain
  useCallbacks (renderPart, renderGroupedPart) sharing the same dep set.
- Tighten test mocks: drop 'any' in the new integration test, hoist the
  MCP delimiter constant above its consumer, and remove the now-stale
  data-variant attribute assertion.

* refactor(tool-call): extract getToolCallId helper and tidy imports

- Pull the (part?.[TOOL_CALL] as Agents.ToolCall)?.id chain into a single
  getToolCallId helper in ContentParts so the three call sites stop
  repeating the cast verbatim.
- Re-sort ToolCallGroup local imports longest-to-shortest per the project
  convention.
- Add a Users mock to the integration test's lucide-react stub so future
  subagent-group tests don't trip over an undefined glyph.

* refactor(tool-call): unnest ternaries in subagent and group labels
2026-05-08 12:29:45 -04:00
Danny Avila
a43bc45b73
🧭 fix: Preserve File Search Upload Target (#13019) 2026-05-08 12:29:45 -04:00
Danny Avila
119ac9c944
📦 chore: bump @librechat/agents to v3.1.80 (#13021) 2026-05-08 12:29:44 -04:00
Danny Avila
eb20d8805d
🐛 refactor: anchor code-generated file lookup on threadFileIds for branched conversations (#13004)
* 🐛 fix: anchor getCodeGeneratedFiles on threadFileIds, not threadMessageIds

In a branched conversation (regenerations producing the same code-output
filename), `getCodeGeneratedFiles` would silently exclude files whose
File-record `messageId` lived on a sibling branch. The user-visible
symptom: "the previous file isn't persisted" — the LLM tries
`load_workbook("output.xlsx")` on turn 2 and gets `FileNotFoundError`
because LC sent `_injected_files: []` to codeapi instead of priming
the prior turn's output.

`claimCodeFile` is keyed by `(filename, conversationId, context)` —
not by messageId. When sibling A first creates `output.csv`, the File
record persists with `messageId = A`. When sibling N (a regeneration
of A's parent) recreates `output.csv`, the claim finds A's record and
`processCodeOutput` deliberately preserves `messageId = A` to keep
file→original-creator provenance intact (correct behavior for the
linear case where the original creator is in-thread).

Turn N+1's `parentMessageId = N`. `getThreadData` walks back from N:
the thread is `[N, root]` — sibling A is NOT in it. The pre-fix query
filtered by `messageId IN [N, root]`, so the file was excluded.

`getCodeGeneratedFiles` already lives next to `getUserCodeFiles`,
which has always filtered by `file_id IN threadFileIds` (the file_ids
referenced by `messages.files[]` arrays during the thread walk). The
asymmetry — user-uploaded files anchored on the message's reference,
code-generated files anchored on the File's own creator — was the
bug. Anchoring both functions on `threadFileIds` reaches the right
files regardless of which sibling first generated them.

`File.messageId` stays informational ("who first generated this") for
provenance and `processCodeOutput`'s "preserve original messageId on
update" logic stays as-is — only the lookup key for thread-scoped
fetches changes.

- `packages/data-schemas/src/methods/file.ts`: signature + filter
  change. JSDoc spells out the branched-conversation rationale.
- `packages/api/src/agents/initialize.ts`: pass `threadFileIds` instead
  of `threadMessageIds`. The local `threadMessageIds` declaration is
  removed since the only consumer is gone.
- `packages/data-schemas/src/methods/file.spec.ts`: 5 new cases:
  - basic happy-path (file referenced by current thread)
  - **the regression**: file's creator messageId is on a sibling
    branch but file_id is in threadFileIds → finds it
  - empty/missing threadFileIds returns []
  - cross-conversation isolation
  - non-execute_code context filter still applies (a chat attachment
    won't be returned even if its file_id is in threadFileIds —
    that's `getUserCodeFiles`'s job)

Applies cleanly on top of dev. When LC #12960 (the typed CodeEnvRef
cutover) lands, the only conflict is the legacy `metadata.fileIdentifier`
metadata key flipping to `metadata.codeEnvRef` — same line, trivial
resolve.

- [x] `cd packages/data-schemas && npx jest src/methods/file.spec` —
  42/42 pass (including the 5 new regression cases)
- [x] `cd packages/api && npx jest src/agents` — 722/722 pass
  (modulo 2 pre-existing summarization e2e failures unrelated)
- [x] `cd api && npx jest server/services/Files server/controllers/agents` —
  432/432 pass
- [x] `npx tsc --noEmit -p packages/api/tsconfig.json` — clean
- [ ] Manual: branched conversation reproducer — generate a file in
  turn 1, regenerate the parent (sibling), then in turn N+1 ask the
  agent to read the file. Pre-fix: `FileNotFoundError`. Post-fix:
  the file is primed and load_workbook succeeds.

* 🧪 test: lock initialize.ts → getCodeGeneratedFiles call shape

Integration-level regression test asserting initializeAgent passes
`threadFileIds` (not `threadMessageIds`) to getCodeGeneratedFiles
in branched-conversation scenarios. Locks in the API shape from the
previous commit, sitting one layer above the data-schemas unit test —
so a future refactor to the priming chain can't silently revert to
the messageId-based filter without surfacing a test failure here.

Two cases:
- The full call shape: agent.tools=['execute_code'], resendFiles=true,
  threadData mock returns distinct messageIds and fileIds. Asserts the
  call uses fileIds, and that getUserCodeFiles uses the same array
  (the symmetric design that closes the sibling-branch hole).
- Empty threadFileIds: getCodeGeneratedFiles is still called with []
  (its own internal early-return handles the empty case); getUserCodeFiles
  is gated at the call site and stays unscheduled.
2026-05-08 12:29:44 -04:00
Danny Avila
e7dbae32e5
🚦 fix: Preserve URL Auto-Submit Startup Config (#13017)
* fix: Preserve URL auto-submit startup config

* test: Cover URL auto-submit interface defaults
2026-05-08 12:29:44 -04:00
Danny Avila
0fe203aaca
🧠 fix: charge Gemini reasoning tokens in agent usage accounting (#13014)
* 🧠 fix: charge Gemini reasoning tokens in agent usage accounting

Resolves #13006.

`usage.ts` previously billed `usage.output_tokens` directly. For Vertex
AI Gemini thinking models, `@langchain/google-common`'s streaming path
emits `output_tokens = candidatesTokenCount` only, dropping
`thoughtsTokenCount`. Reasoning was billed at zero and the
`total_tokens === input_tokens + output_tokens` invariant was broken.

The fix lives in agents (danny-avila/agents#157) — but this is also a
defense-in-depth backstop in case agents misses a path or another
provider exhibits the same shape. `resolveCompletionTokens(usage)` adds
`output_token_details.reasoning` back when (and only when) the gap is
present (`total - input > output`), so providers that already include
reasoning in `output_tokens` (OpenAI o-series, Anthropic, the
Google-API wrapper) are no-ops — no double-counting.

- `SplitUsage` gains a `completion` field; all four billing call sites
  in `processUsageGroup` use it instead of `usage.output_tokens`.
- `total_output_tokens` in the result also reflects the corrected
  count.
- `UsageMetadata` interface in `IJobStore.ts` adds the
  `output_token_details` field for type safety.
- 4 new tests in `usage.spec.ts` cover: Vertex undercount fix, OpenAI
  no-double-count, structured spend path with cache + reasoning, no-op
  when no details present.

* 🩹 fix: simplify reasoning correction to invariant-based gap check

Initial fix gated the correction on `output_token_details.reasoning > 0`,
which doesn't help in the live failure case: when google-common's stream
emits the buggy fallback usage_metadata, output_token_details is empty
({}) and the gate exits early.

Live debugging showed the reliable signal is the documented invariant
itself: `total_tokens === input_tokens + output_tokens`. When buggy
streams undercount output, total exceeds input + output by exactly the
unbilled reasoning. Use `total - input` as the corrected output.

This is provider-agnostic and stays a no-op for compliant providers
(OpenAI/Anthropic/Google-via-CustomChatGoogleGenerativeAI), where the
gap is zero.

Live verified end-to-end against gemini-3-flash-preview:
- With agents fix in place:  output_tokens=437 → billed 437  (no-op)
- Backstop only (no agents fix, buggy input): raw 135, billed 297
  (= total 309 - input 12, matches actual API charge)

Updated tests to cover both scenarios.
2026-05-08 12:29:43 -04:00
Danny Avila
93c4ef4ba8
🧱 refactor: typed CodeEnvRef + kind discriminator + principal-aware sandbox cache (#12960)
* 🧱 refactor: typed CodeEnvRef + kind discriminator + tenant-aware sandbox cache

Final cutover for the LibreChat ↔ codeapi sandbox file identity. Replaces
the magic string `${session_id}/${file_id}?entity_id=...` with a typed,
discriminated `CodeEnvRef`. Pre-release lockstep deploy with codeapi
#1455 and agents #148; no legacy aliases retained.

## Final shape

```ts
type CodeEnvRef =
  | { kind: 'skill'; id: string; storage_session_id: string; file_id: string; version: number }
  | { kind: 'agent'; id: string; storage_session_id: string; file_id: string }
  | { kind: 'user';  id: string; storage_session_id: string; file_id: string };
```

`kind` drives codeapi's sessionKey: `<tenant>:<kind>:<id>[✌️<version>]`
for shared kinds, `<tenant>:user:<userId>` for user-private (auth context
provides `userId`). `version` is statically required for `kind: 'skill'`
and forbidden otherwise via discriminated union — constraint holds at
compile time on every consumer, not just codeapi's runtime validator.

`id` is sessionKey-meaningful for `'skill'` / `'agent'`; informational
only for `'user'` (codeapi resolves user identity from auth context).

## What changed

- `packages/data-provider/src/codeEnvRef.ts` — discriminated union +
  `CODE_ENV_KINDS` const-tuple keeps the runtime list and TS union
  locked together.
- Schemas: `metadata.codeEnvRef` and `SkillFile.codeEnvRef` enums
  tightened to `['skill', 'agent', 'user']`.
- `primeSkillFiles` writes `kind: 'skill'`, `id: skill._id`,
  `version: skill.version`. Cache-hit path reads `codeEnvRef`
  directly. Bumping `skill.version` on edit naturally invalidates
  the prior cache entry under the new sessionKey.
- `processCodeOutput` writes `kind: 'user'`, `id: req.user.id`. Output
  bucket is always user-scoped, regardless of which skill the
  execution invoked. New regression test pins the asymmetry.
- `primeFiles` reupload preserves `kind`/`id`/`version?` from the
  existing ref so a skill-cache-miss reupload doesn't silently demote
  to user bucket.
- `crud.js` upload functions (`uploadCodeEnvFile` /
  `batchUploadCodeEnvFiles`) thread `kind`/`id`/`version?` to the
  multipart form (codeapi #1455 option α). Without these on the wire,
  codeapi falls back to user bucketing and skill-cache invalidation
  never fires. Client-side validation mirrors codeapi's validator.
- `Files/process.js` — chat attachments use `kind: 'user'`; agent
  setup files use `kind: 'agent'`.
- Drops `entity_id` everywhere (struct, schema sub-docs, write paths,
  upload form fields). Drops `'system'` from the kind enum (no emitter
  ever existed).

## Test plan

- [x] `cd packages/data-provider && npx jest src/codeEnvRef.spec` — 4 / 4
- [x] `cd packages/data-schemas && npx jest` — 1447 / 1447
- [x] `cd packages/api && npx jest src/agents` — 81 / 81 in skillFiles +
  handlers + resources
- [x] `cd api && npx jest server/services/Files server/controllers/agents` —
  436 / 436
- [x] `cd api && npx jest server/services/Files/Code` — 98 / 98 (incl.
  new "outputs are user-scoped regardless of which skill the execution
  invoked" regression and "reupload forwards kind/id/version from
  existing ref")
- [x] `npx tsc --noEmit -p packages/data-{provider,schemas}/tsconfig.json
  && npx tsc --noEmit -p packages/api/tsconfig.json` — clean (only
  pre-existing unrelated dev errors in storage/balance, untouched here)

## Deploy notes

- **24h cache-miss burst** on first deploy. Inputs (skill caches re-prime
  under new sessionKey shape) and outputs (any pre-Phase C skill-output
  cached files become unreadable). Bounded by codeapi's 24h TTL.
- **Lockstep with codeapi #1455 and agents #148.** Either repo can land
  first since no aliases to drain, but the three deploys must overlap
  within the same maintenance window.
- **`@librechat/agents` bump to `3.1.79-dev.0`** required after agents
  #148 lands and is published.

## What this enables

Auth bridge work (JWT-based tenant/user identity between LC and codeapi)
— codeapi now derives sessionKey purely from `req.codeApiAuthContext.{
tenantId, userId}`, so the next chapter is replacing the header-asserted
user identity with a verified-claim path.

* 🩹 fix: persist execute_code uploads under codeEnvRef metadata key

Codex review P1 (chatgpt-codex-connector). `Files/process.js` was
storing the upload result under `metadata.fileIdentifier` even though:
- `uploadCodeEnvFile` now returns `{ storage_session_id, file_id }`,
  not the legacy magic string.
- The post-cutover schema (`File.metadata.codeEnvRef`) only declares
  `codeEnvRef` — mongoose strict mode silently strips unknown keys.
- All readers (`primeFiles`, `getCodeFilesByIds`,
  `categorizeFileForToolResources`, controller filtering) check
  `metadata.codeEnvRef`.

Net effect of the bug: chat-attached and agent-setup execute_code files
would lose their sandbox reference on save, and primeFiles would skip
them on subsequent code-execution turns — the file blob would still be
available locally but never re-mounted in the sandbox.

Fix: construct the full `CodeEnvRef` (`{ kind, id, storage_session_id,
file_id }`) at the write site and persist under `metadata.codeEnvRef`.
`BaseClient`'s "is this a code-env file" presence check accepts the new
shape alongside the legacy `fileIdentifier` for back-compat with any
pre-cutover records still in the database. Mirrors the same change in
`processAttachments.spec.ts` (which re-implements the BaseClient logic
for testability).

New regression tests in `process.spec.js` cover three cases:
- chat attachments (`messageAttachment=true`) → `kind: 'user'`
- agent setup (`messageAttachment=false`) → `kind: 'agent'`
- legacy `fileIdentifier` key is NOT persisted (would be schema-stripped)

* 🩹 fix: read storage_session_id on primed file refs (Codex P1)

Codex review (chatgpt-codex-connector). After Phase B's per-file
`session_id` → `storage_session_id` rename, `primeFiles` emits the
new field — but `seedCodeFilesIntoSessions` was still reading
`files[0].session_id` for the representative session and `f.session_id`
for the dedupe key. In runs with only primed attachments (no skill
seed), `representativeSessionId` was `undefined`, the function
returned the unchanged map, and `seedCodeFilesIntoSessions` silently
dropped the entire batch. The first `execute_code` call then started
without `_injected_files` and the agent couldn't see prior-turn
artifacts.

Fix:
- `codeFilesSession.ts`: read `f.storage_session_id` for both the
  dedupe key and the representative session id. JSDoc updated to
  match the new field name.
- `callbacks.js`: the two output-file persistence paths read
  `file.session_id` to pass to `processCodeOutput` — switch to
  `file.storage_session_id`. The original comment explicitly says
  this should be the STORAGE session, which is exactly the field
  Phase B renamed.
- `codeFilesSession.spec.ts`: fixture builder uses `storage_session_id`
  and `kind: 'user'` to match the post-cutover `CodeEnvFile` shape.

Lockstep coordination: this matches the post-bump shape of
`@librechat/agents` 3.1.79+. CI tsc errors against the currently-pinned
3.1.78 are expected and resolve when the dep bumps in this PR before
merge.

* 📦 chore: Bump `@librechat/agents` to version 3.1.80-dev.0 in package-lock and package.json files

* 🪪 fix: thread kind/id/version through codeapi /download URLs (Phase C α)

Symmetric fix for the upload-side wire change in 537725a. Codeapi's
`sessionAuth` middleware now requires `kind`/`id`/`version?` on every
download/freshness URL — without them it 400s with "kind must be one
of: skill, agent, user" before serving the file.

Three sites construct codeapi-side URLs that go through `sessionAuth`:

- `processCodeOutput` (`Files/Code/process.js`): `/download/<sess>/<id>`
  for freshly-generated sandbox outputs. Always `kind: 'user'` +
  `id: req.user.id` — code-output files are always user-private,
  regardless of which skill the run invoked.
- `getSessionInfo` (`Files/Code/process.js`): `/sessions/<sess>/objects/<id>`
  for the 23h freshness check. Pulls kind/id/version straight off the
  `codeEnvRef` already in scope — skill files stay skill-bucketed,
  user files stay user-bucketed.
- `/code/download/:session_id/:fileId` LC route (`routes/files/files.js`):
  proxies to codeapi for manual downloads. Code-output files only on
  this route, so `kind: 'user'` + `id: req.user.id`.

The `getCodeOutputDownloadStream` helper in `crud.js` now takes an
`identity` param, validated by a `buildCodeEnvDownloadQuery` helper
that mirrors `appendCodeEnvFileIdentity`'s shape rules: kind required
from the closed `{skill, agent, user}` set, version required for
'skill' and forbidden otherwise. Bad callers fail fast on the client
instead of round-tripping a 400.

Also cleans up two log-noise sources reported alongside the 400:

- `logAxiosError` in `packages/api/src/utils/axios.ts` was dumping
  `error.response.data` raw. With `responseType: 'arraybuffer'` that's
  a `Buffer` (~4 chars per byte after JSON-serialization); with
  `responseType: 'stream'` it's a `Readable` whose internal state
  serializes the entire ring buffer + socket. New `renderResponseData`
  decodes small buffers as UTF-8 (truncated past 2KB) and stubs streams
  as `'[stream]'`. Diagnostics stay useful, log lines stop being
  megabytes.
- `/code/download` route's catch was bare `logger.error('...', error)`,
  bypassing the redactor. Switched to `logAxiosError` so it benefits
  from the same buffer/stream handling.

Tests updated to match the new contract:
- crud.spec: `getCodeOutputDownloadStream` fixtures pass `userIdentity`;
  new cases cover skill identity (with version), bad kind rejection,
  skill-without-version rejection.
- process.spec: `getSessionInfo` test passes a full `codeEnvRef` object.

* ♻️ refactor: extract codeEnv identity helpers into packages/api

Per the project convention that new backend code lives in TypeScript
under `packages/api`, moves `appendCodeEnvFileIdentity` and
`buildCodeEnvDownloadQuery` from `api/server/services/Files/Code/crud.js`
into a new `packages/api/src/files/code/identity.ts` module.

Both helpers are pure validators that mirror codeapi's
`parseUploadSessionKeyInput` server-side rules (closed kind set,
`version` required for `'skill'` and forbidden otherwise) — they
deserve TS support and a dedicated spec rather than living as
JSDoc-typed helpers in the legacy `/api` workspace. The new module:

- Exports a `CodeEnvIdentity` interface using the
  `librechat-data-provider` `CodeEnvKind` discriminated union.
- Adds 13 unit tests in `identity.spec.ts` covering the validation
  matrix (skill+version, agent, user, and every rejection path) plus
  URL encoding for the download query.
- Re-exported from `packages/api/src/files/code/index.ts` alongside
  `classify`, `extract`, and `form`.

Consumer updates:
- `api/server/services/Files/Code/crud.js`: drops the local helpers
  and imports them from `@librechat/api`. Net -64 lines.
- `api/server/services/Files/Code/process.js`: same.
- Test mocks for `@librechat/api` in three spec files now stub the
  helpers' validation behavior locally rather than pulling them
  through `requireActual` (which would drag in provider-config
  init-time side effects). The package's `exports` field only
  surfaces the root barrel, so leaf imports aren't reachable from
  legacy `/api` test setup.

No runtime behavior change. Identity validation rules and emitted
form/query shapes are byte-for-byte identical pre/post.

* 🪪 fix: emit resource_id alongside id on _injected_files (skill 403 fix)

Companion to codeapi #1455 fix and agents 3.1.80-dev.1 — the wire
shape for shared-kind files now requires `resource_id` distinct from
the storage `id`. Without this LC change, codeapi's sessionKey
re-derivation on every shared-kind /exec rejects with 403
session_key_mismatch:

    cached:  legacy:skill:69dcf561...✌️59  (signed at upload, skill _id)
    derived: legacy:skill:ysPwEURuPk-...✌️59  (storage nanoid)

Emit sites updated:

- `primeInvokedSkills` cache-hit path: `resource_id: ref.id` (the
  persisted skill `_id` from `codeEnvRef.id`); `id: ref.file_id`
  unchanged (storage uuid).
- `primeInvokedSkills` fresh-upload path: `resource_id: skill._id.toString()`
  on every primed file (the `allPrimedFiles` builder type now carries
  the field).
- `processCodeOutput`'s `pushFile` (Code/process.js): `resource_id: ref.id`
  — for `kind: 'user'` this is informational (codeapi derives
  sessionKey from auth context) but emitted for shape uniformity
  with shared kinds.

Bumps `@librechat/agents` to `^3.1.80-dev.1` (the version that
ships the matching `CodeEnvFile.resource_id` field).

## Test plan

- [x] `cd packages/api && npx jest src/agents` — 67 / 67 pass
  (skillFiles fixtures updated to assert `resource_id` on the
  emitted CodeSessionContext.files).
- [x] `cd api && npx jest server/services/Files server/controllers/agents` —
  445 / 445 pass (process.spec fixtures updated for the reupload
  + cache-hit emission).
- [x] `npx tsc --noEmit -p packages/api/tsconfig.json` — clean.

* fix(skill-tool-call): carry resource_id through primeSkillFiles → artifact

Codeapi was 400ing every /exec following a `handle_skill` tool call
with `resource_id is invalid` (`type: 'undefined'`). Both code paths
in `primeSkillFiles` (cache-hit + fresh-upload) returned files
without `resource_id`/`kind`/`version`, and the artifact in
`handlers.ts` forwarded the stripped shape into
`tc.codeSessionContext.files` → `_injected_files`.

`primeInvokedSkills` (the NL-detected loader) had already been fixed
end-to-end; this commit aligns the tool-invoked path with the same
contract: `resource_id` = `skill._id.toString()`, `kind: 'skill'`,
`version` = the skill's monotonic counter.

Tests added to `skillFiles.spec.ts` lock the contract on
`primeSkillFiles` directly so future refactors can't silently drop
the resource identity again.

* fix(handlers.spec): align session_id → storage_session_id rename + kind discriminator

Pre-existing TS errors against the post-rename `CodeEnvFile` shape:
the test file still used `session_id` on per-file objects (renamed to
`storage_session_id` in agents Phase B/C) and was missing the `kind`
discriminator the discriminated union requires. Both inputs and the
matching `expect.toEqual(...)` mirrors updated together so the
runtime equality check still holds.

Lines 723-732 stay as-is — they sit behind `as unknown as
ToolCallRequest` and TS already skipped them.

* chore: fix `@librechat/agents`, correct version to 3.1.80-dev.0 in package.json files

* chore: bump `@librechat/agents` to version 3.1.80-dev.1 in package.json and package-lock.json

* chore: bump `@librechat/agents` to version 3.1.80-dev.2

* feat(observability): trace file priming chain from primeCodeFiles to _injected_files

Diagnosing the user-upload "files=[] on first /exec" bug requires
seeing where in the LC chain a file ref disappears. Prior to this
patch the chain (primeCodeFiles → primedCodeFiles → initialSessions
→ CodeSessionContext → _injected_files) was opaque end-to-end:
  - primeCodeFiles silently dropped files without `metadata.codeEnvRef`
  - reuploadFile catches all errors and continues with no signal
  - the handlers.ts handoff to codeapi never logged what it was sending

After this patch, a single grep on `[primeCodeFiles]` plus
`[code-env:inject]` shows the full per-file path:

  [primeCodeFiles] in: file_ids=N resourceFiles=M
  [primeCodeFiles] file=<id> path=skip reason=no-codeenvref filename=...
  [primeCodeFiles] file=<id> path=cache-hit-by-session storage_session_id=...
  [primeCodeFiles] file=<id> path=reupload reason=no-uploadtime ...
  [primeCodeFiles] file=<id> path=reupload reason=stale ...
  [primeCodeFiles] file=<id> path=reupload-success oldSession=... newSession=... newFileId=...
  [primeCodeFiles] file=<id> path=reupload-failed session=...
  [primeCodeFiles] file=<id> path=fresh-active storage_session_id=...
  [primeCodeFiles] out: returned=N skippedNoRef=M reuploadFailures=K

  [code-env:inject] tool=<name> files=N missingResourceId=K     (debug)
  [code-env:inject] M/N files missing resource_id ...           (warn)
  [code-env:inject] tool=<name> _injected_files=0 ...           (warn)

The boundary log warns when LC sends zero injected files on a
code-execution tool call — that's the user's actual symptom showing
up at the LC side instead of having to correlate against codeapi's
`Request received { files: [] }`.

Tag chosen as `[code-env:inject]` rather than `[handoff:exec]` to
avoid collision with the app-level "handoff" semantic (subagent
handoff workflow).

Structural cleanup in primeFiles: replaced the `if (ref) { ... }`
nesting with an early `if (!ref) continue` so the per-path
instrumentation hooks land at top-level scope instead of indented
inside a conditional. Behavior unchanged; pushFile / reuploadFile
identical.

Spec fixtures (handlers.spec.ts, codeFilesSession.spec.ts) updated
to include `resource_id` on `CodeEnvFile` literals — required by
the post-3.1.80-dev.2 type now installed.

## Test plan

- [x] `cd packages/api && npx jest src/agents/handlers.spec.ts src/agents/codeFilesSession.spec.ts src/agents/skillFiles.spec.ts` — 69/69 pass
- [x] `cd api && npx jest server/services/Files/Code/process.spec.js` — 84/84 pass
- [x] `npx tsc --noEmit -p packages/api` — clean
- [x] `npx eslint` on all four touched files — clean

* chore: add CONSOLE_JSON_STRING_LENGTH to .env.example for JSON log string length configuration

* fix(files): align codeapi upload filename with LC's sanitized DB filename

User-attached files for code execution were uploading to codeapi
under `file.originalname` (raw upload filename, may contain spaces /
special chars) while LC's DB record stored the sanitized form
(`sanitizeFilename(file.originalname)`, underscores). Codeapi
preserves whatever filename the upload sent, so the sandbox saw
`/mnt/data/<originalname>` while LC's `primeFiles` toolContext text
+ `_injected_files.name` referenced `file.filename` (sanitized).

Visible failure: agent gets system prompt saying

    /mnt/data/librechat_code_api_-_active_customer_-_2025-11-05.xlsx

…tries that path, hits `FileNotFoundError`, then notices the
sandbox's actual `Available files` line says

    /mnt/data/librechat code api - active customer - 2025-11-05.xlsx

…retries with spaces, succeeds. Wastes a tool call per upload and
leaks raw filenames into model context.

Fix: sanitize once and use the sanitized form in both the codeapi
upload AND the LC DB record. Sandbox path = LC toolContext text =
in-memory ref name. No drift.

Reupload path (`Code/process.js` line 867 `filename: file.filename`)
already uses the sanitized DB name, so it stays consistent with the
fresh-upload path after this change.

## Test plan

- [x] `cd api && npx jest server/services/Files/process` — 32/32 pass
- [x] `npx eslint` on the touched file — clean

* chore: bump `@librechat/agents` to version 3.1.80-dev.3 in package.json and package-lock.json
2026-05-08 12:29:43 -04:00