[pull] main from danny-avila:main - #267
Merged
Merged
Conversation
* fix: Enable programmatic Bash in selected attached workspaces * Centralize project PTC construction and preserve recognized capabilities * Prevent stale list refreshes from restoring removed pins
* 🎚️ fix: Allow Opting Out of the MCP OAuth `resource` Parameter Adds `mcpServers.<name>.oauth.send_resource_parameter`, defaulting to true, so operators whose authorization server rejects RFC 8707 `resource` can complete the MCP OAuth flow. Suppression applies at send time only: Protected Resource Metadata is still discovered, still validated against the server URL (RFC 9728 §3.3) and still recorded on the stored client binding. * 🧹 chore: Drop Accidentally Tracked node_modules Symlinks Worktree-local symlinks were committed because .gitignore matches node_modules/ as a directory, not as a symlink of that name. They pointed outside the repository, so CI dependency installation failed and every downstream check with it. * 🧪 test: Cover the MCP OAuth `resource` Opt-Out at Token Exchange The authorization_code exchange reads the decision from flow metadata rather than live config, so a code obtained without `resource` is never exchanged with one. Covers both the opted-out flow and a flow initiated before the field existed, which must keep sending the parameter. * 💅 style: Apply Prettier to the Token Exchange Test * 🔒 fix: Make the MCP OAuth `resource` Opt-Out Actually Take Effect Addresses three Codex findings on the opt-out: - `startAuthorization` copies the configured `authorization_url` verbatim, so a `resource` an admin left in that URL survived into the request and Entra kept rejecting the flow. Opting out now deletes the parameter on both authorize paths instead of only declining to add one. - `.omit()` let a user-managed submission carrying `send_resource_parameter` parse successfully while discarding the field, so the opt-out was silently ignored. It is now `z.never().optional()`, matching the documented contract and the `audience` precedent. - A pending flow built with `resource` could be replayed for the full pending window after an operator set the option, reissuing the request they had just reconfigured away from. `isCurrentServerOAuthFlow` now compares the captured decision with live config. Stored tokens are unaffected, so changing the option still does not force re-authentication. * 🧯 fix: Suppress Inherited MCP OAuth `resource` and Stale Replays Addresses two further Codex findings on the opt-out: - The MCP SDK uses `token_endpoint` verbatim and the refresh paths post to the resolved token URL as-is, so a `resource` an admin left in `token_url` (or one on a discovered token endpoint) still reached the provider. The outbound URL is now sanitized at the exchange and in all three refresh paths. Stored metadata keeps its configured form so the client binding still matches. - Pending-flow replay was only guarded inside MCPConnectionFactory, so the browser initiate endpoint could still redirect to the stored URL. The shared pending helper now takes optional live config and refuses a replay whose captured decision no longer matches; the route passes it. * 🪤 fix: Scope the MCP OAuth `resource` Opt-Out to Paths It Can Serve Correctly The browser initiate route compared the pending flow against `resolveConfigServers`, which deliberately omits unmodified YAML servers (see `mcp/context.ts`). For the very config this option targets that read a captured `false` against `undefined`, reported drift on every initiation, and then returned 400 because factory-written flow states carry no `oauth` property to rebuild from. Reverted rather than patched: doing it right needs effective-config resolution behind a `packages/api` helper, per the `/api` wiring-only rule, and this sandbox cannot run that route suite. Retained and corrected: - The pending-flow guard no longer rides on `isCurrentServerOAuthFlow`, which also gates reuse of a recent COMPLETED flow. It was invalidating already-issued tokens, contradicting the promise that changing this option never forces re-authentication. Now a PENDING-only predicate at the two replay sites. - `ServerConfigsDB.sanitizeUserManagedOAuthConfig` strips `send_resource_parameter`, so a DB-backed or imported user config cannot activate the admin-only opt-out. --------- Co-authored-by: Lia <lia@librechat.ai>
* 🧼 fix: Strip Cache-Bust Query Before Local Vision Encode Reused code-interpreter images persist filepath with a ?v= suffix. prepareImagesLocal now strips it before disk encode, matching crud/share. Co-authored-by: Zsanz3 <Zsanz3@users.noreply.github.com> * 🧪 test: Isolate prepareImagesLocal query-strip coverage Mock sharp and resize so the local vision-encode test does not load the rest of the file-strategy graph. Co-authored-by: Zsanz3 <Zsanz3@users.noreply.github.com> * 🪒 fix: Strip Cache-Bust Suffixes From Every Local File Read Reused code-interpreter outputs persist a `?v=<timestamp>` suffix on the file document's filepath, and local storage resolves that field into a filesystem path. prepareImagesLocal already handled it after the cherry-picked commits; getLocalFileStream did not, so every download-stream consumer (the download route, provisioning, agent and skill file reads) still hit ENOENT for a regenerated image. Move the strip into one documented helper and call it from all three local read paths. * 🧱 refactor: Move Cache-Bust Stripping Into the Storage Package `/api` holds wiring, not behavior, so `stripCacheBust` moves out of a new CJS helper and into `packages/api/src/storage/path.ts`, beside `resolveDownloadPath` — the function that already decides what a stored filepath means to a download. Its doc comment now states why the two differ: a remote strategy's query string can carry a presigned signature, so only the local paths may strip. The local storage modules keep just the call into it. Semantics are covered by the storage package's own test; the `/api` specs pin the wiring. Addresses the Codex P1 finding on 5f8dbf3. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Zsanz3 <Zsanz3@users.noreply.github.com> Co-authored-by: Lia <lia@librechat.ai>
* Load authorized workspace repository instructions into agent context * Unify repository instruction loading across agent ingresses * Await cancellation regression assertion directly * Annotate the shared instruction loader for declaration builds * Expose repository read budget and inject the host-owned cache
* fix: Clarify Background Tool Handoffs and Validation Feedback * Preserve background handle wire compatibility * Align error logging regression assertions
Co-authored-by: Lia <lia@librechat.ai>
* 🏛️ fix: Resolve Global Access Roles for Tenant Owner Grants * fix: Execute the base-role fallback inside the system context * fix: Resolve every tenant role lookup from one visible set * fix: Merge the visible role set before any filtering * perf: Resolve Visible Tenant Roles in One Query 🏘️ --------- Co-authored-by: lia-by-librechat[bot] <328778573+lia-by-librechat[bot]@users.noreply.github.com> Co-authored-by: Lia <lia@librechat.ai> Co-authored-by: Danny Avila <danny@librechat.ai>
* 🔌 feat: OpenAPI spec and docs for the public Agents API
* 🔧 fix: address review on Agents API OpenAPI spec and docs:
- adapter: mark JSON request bodies as required
- openapi.js: move gate, config read and spec serving into packages/api
as an injected router factory; leave /api as wiring only
- docs: compute URLs in-browser and use a relative servers url so docs
work under a base path (e.g. /chat)
- build: copy the generated spec into dist so the multi-stage image ships it
- agents: reuse the enforced list envelope for the docs so the AgentList
schema cannot drift from the runtime cursor and strictness
* 🔧 fix: align Agents API OpenAPI contract with enforced runtime
Second review round on the public Agents API spec. Three cases where the
published contract described something the server does not do, so generated
clients would fail against the live API:
- Upload form: document the `purpose` field (enum: file_search, execute_code,
context) that POST /agents/{id}/files actually requires, instead of the
internal-only `tool_resource`; requests following the old form were rejected
with 400 because `purpose` was absent.
- Auth: drop the `apiKeyBearer` scheme from the management and skill endpoints.
They gate on OIDC/M2M via requireAgentManagementAuth and deliberately reject
the API-key fallback; the API key belongs to the out-of-scope
OpenAI-compatible endpoints and returns when those are documented.
- Skill update: document the 409 conflict PATCH /skills/{id} returns on a stale
expectedVersion. The agent update path collapses its internal 409 to 400, so
it correctly keeps no 409.
* 🔧 fix: serve docs on experimental server and document the 401
Third review round on the public Agents API spec.
- Experimental server: mount routes.openapi in api/server/experimental.js before its /api 404 handler. Without it, npm run backend:experimental returns 404 for /api/docs and /api/openapi.json even when openapi.enabled is true, because that server never mounted the router.
- Auth failure: document the 401 that requireAgentManagementAuth returns on a missing, expired, malformed, or unbound token. Its body is { "error": "Unauthorized" }, a flatter shape than the management error schema, so add a dedicated UnauthorizedError schema and a shared 401 response to the agent and skill contracts rather than reusing the standard error schema.
* 🔧 fix: document skill-update, auth-conflict, upload-limit, and watch-copy gaps
* 🔧 fix: added error shapes for permission denied and internal server errors in API responses docs
* fix: cover the auth-layer 500 shape and pin the account-deletion code
* fix: update OpenApiRouterDeps documentation and simplify request handling in isEnabled function
* 🔧 fix: gate the Swagger asset mount behind the OpenAPI toggle
* 🔧 fix: accept the malformed-JSON 400 envelope in the contract
* fix: accept the tenant-isolation 403 envelope in the contract
* fix: add description for content property in SkillFileUpdateRequest to clarify byte limit
…ns` (#16029) Regenerates packages/api/openapi/agents.openapi.json from the code so openapi:check passes again. #16008 added repositoryInstructions to the agent schema and merged before #15928, whose committed spec was generated from a branch that predated the field, leaving dev red. Co-authored-by: Lia <lia@librechat.ai>
…15988) * 📪 feat: Send Per-Request MCP Headers Without Hiding the Tool Catalog A `{{LIBRECHAT_BODY_*}}` placeholder in an MCP server's `headers` marks the server request-scoped, and catalog discovery has no conversation to resolve it against, so LibreChat skipped tool discovery and the server appeared with no individual tools. Operators can now declare those headers separately in `requestHeaders`, which discovery omits and chat turns resolve and merge over `headers`. * fix: normalize requestHeaders at each resolution entry point Registering the new field in every consumer that enumerates header-bearing config fields left five gaps: startup inspection, case-insensitive override, the admin-configurable comparison surface, Graph token preprocessing, and direct-bearer recovery. Fold `requestHeaders` into `headers` at the entry of each resolution pipeline instead, so Graph preprocessing, direct-bearer detection, `processMCPEnv` and the transports keep reading one header map and never learn a second exists. Catalog paths strip it at the same boundary, startup inspection included. * fix: keep servers with chat-only headers off the shared app connection A static `requestHeaders` map left a server eligible for app-level sharing, and `ConnectionsRepository.loadConnection` handed the raw config to the factory, so the shared session's own `initialize` and `tools/list` carried headers documented as chat-only — and every later catalog read reused that session. One session cannot serve both sides of the field. Exclude such configs from `canUseAppConnection`, the single predicate both `connectAppServers` and the repository's own app-level gate already consult, so the chat turn keeps its headers on a user-scoped connection while catalog work never sees them. * fix: normalize requestHeaders before the manager's bearer decisions `createUserConnectionInternal` reads `usesDirectOpenIDBearerRecovery` and `resolveDirectOpenIDBearerConfig` on the config it receives, while the merge sat in `resolveRuntimeConfig`, a leaf helper below those calls. An `Authorization` template declared in `requestHeaders` therefore never entered direct-bearer mode: a `tools/list` 401 skipped the force-refresh and the connection was returned with a stale bearer. Normalize at the birth of that pipeline's config instead, ahead of all four decisions, and make `getMCPAppToolsPublicationGeneration` normalize before hashing. Connection paths hold a merged config while cache paths hold the declared one, so without a canonical hash the two would address different catalog slices. * fix: Preserve MCP request header ownership across lifecycle boundaries * fix: Narrow remote header identity before rebuilding config * fix: Preserve request header precedence over catalog API keys * docs: Synchronize generated agent API schemas * fix: Ignore shadowed generated MCP user key requirements --------- Co-authored-by: Lia <lia@librechat.ai> Co-authored-by: Danny Avila <danny@librechat.ai>
* 🪜 feat: Trace Viewer Steps, Sequence Scale and Previews * 🪜 fix: split tool-only rounds by run step; preview only a step's own roots * 🪜 fix: keep windows and previews on the records they describe across pages, modes and failed wrappers * 🪜 fix: keep step folds and previews honest across page boundaries and failed turns * 🪜 fix: causal tie-breaks, final-only previews, span-shaped title runs, and truncating long names * 🪜 fix: index previews in one pass, split agent handoffs, withhold parallel lanes * 🪜 fix: skip the holes a streaming message leaves in its content * 🪜 fix: reserve a round for compaction summaries; keep parallel-lane tools with their own model call * 🪜 fix: record a handoff that starts with reasoning; look lanes up in a map * 🪜 fix: per-lane leading steps, wrappers for turns without work yet, no final alignment for runs that ended early * 🪜 fix: append to a step's per-tool bucket instead of copying it per call * fix: Preserve Trace Preview Alignment Across Run Boundaries * perf: Cache Trace Ancestors and Lane Branches
* 📭 fix: Deliver Attachment Text Until a File Tool Holds It A `none`-routed upload had its extracted text withheld whenever a file tool was enabled, on the assumption the tool would serve the file. A plain chat with the File Search toggle runs an ephemeral agent, so the upload is filed under no tool resource and never embedded: the text was withheld for a vector store that never received the file, and the attachment reached nothing. Withholding now requires the record to show the tool holds the file, which is the same evidence deferred provisioning reads before queueing it. `resources.ts` delegates to that shared predicate so the two cannot disagree again. * test: Give the Code-Running Primary Agent a Sandbox Copy --------- Co-authored-by: Lia <lia@librechat.ai>
- The Anthropic thinking-budget description dropped 'to': 'Claude is allowed use for' -> 'allowed to use for'. - The reasoning-summary description rendered 'none,auto' without a space after the comma. Only the English locale file is touched. Signed-off-by: simpleqt <89645338+simpleqt@users.noreply.github.com>
* 🗣️ fix: Keep Provider Error Text on Unclassified Agent Failures An upstream failure LangChain does not classify discarded the provider's own message and answered with the generic upstream sentence plus a status, so a gateway or privacy-proxy rejection lost the only account of what happened. The provider text now rides along in the persisted payload as `message`, withheld only where a content policy inspects the traffic — the same condition `getUserFacingProviderError` and `getUserFacingRequestError` already decide by. * fix: Harden Provider Error Detail Retention --------- Co-authored-by: Danny Avila <danny@librechat.ai>
* fix: Serialize MCP OAuth token refresh across replicas `MCPTokenStorage.inflightRefreshes` coalesces refresh-token redemptions inside one Node process. Behind a load balancer without session affinity, one user's concurrent requests land on different replicas, each reads the same not-yet-rotated refresh token and redeems it. RFC 9700 servers treat the second redemption as replay and revoke the whole grant family, so the user is asked to authorize the MCP server again. `forceRefreshTokens` now takes a cross-replica refresh flight before redeeming, using the same `FlowStateManager.acquireLease` primitive the OAuth teardown fence uses under a distinct key. A replica that waited for the flight adopts the tokens the holder rotated instead of redeeming again; when the flight is still held after the wait window it falls back to the unfenced redemption every earlier release performed. * fix: Redeem unfenced when the refresh flight lease store fails * fix: Fence MCP refresh waiters instead of redeeming unfenced A waiter that could not take the cross-replica refresh flight fell through to an unfenced redemption after 10s, while the holder's own stale abort does not fire until 60s. A refresh taking 11 to 60 seconds therefore still let two replicas redeem one refresh token, the replay this fence exists to prevent. The waiter now polls until it either adopts the tokens the holder rotated or acquires the flight, and fails as MCPTokenRefreshUnavailableError rather than redeeming beside a live holder. `getTokens` callers defer connection recovery on that error, and the stored credential is left intact for a later attempt. The flight is keyed by the stored credential (tenant, user, server name) rather than the caller's OAuth binding digest, so a rolling config change cannot hand two replicas different locks over one stored token. The wait is an operator lever, `oauthRefreshWaitTimeout`, clamped to half the stale window. An adoption read that fails no longer gives up the held flight. * fix: Annotate derived refresh-flight constants for isolatedDeclarations * fix: Recapture the credential generation a peer published on adoption * fix: Hold the MCP refresh flight until redemption settles Four corrections to the cross-replica refresh flight. The flight lease equalled the window that aborts a stalled redemption, on the claim that an expired flight could never belong to a redemption still able to reach the token endpoint. Aborting proves no such thing: the request may have been processed with its response lost, and a stalled event loop can delay the abort past its own deadline. The lease now outlives the abort, so a peer cannot redeem a credential the provider has already rotated. A live replica still releases on settle, so the margin is paid only by one that died holding the flight. The credential snapshot was taken after the first failed acquisition, so a holder that stored and released in that gap was snapshotted post-rotation: the next attempt saw an unchanged record and redeemed the credential it should have adopted. It is taken before the first attempt now. `oauthRefreshWaitTimeout` accepted zero while the runtime mapped every non-positive value to the default, so the config validated and then behaved contrary to its value. Zero is rejected at load. Adoption announced a second credential change through `handleOAuthRefreshSuccess`, whose `onOAuthCredentialsChanged` advances authorization state after persistence this replica did not perform. That retired the generation recaptured beside it and fenced the build against its own tool publication. Adoption now updates the local token-flow cache and recaptures, without announcing. * fix: Check every held MCP refresh flight for a peer's rotation Four corrections, two of them consequences of moving the credential observation ahead of the first lease attempt. That move created a storage read inside `beginRefreshFlight`, and its failure reached a handler written for a lease-store outage, so a transient read error became an unfenced redemption beside a live peer. The flight now handles its own reads: losing the observation costs adoption and nothing else, and only the lease store failing may redeem unfenced. The observation was also never compared when the first acquisition succeeded. A peer that rotated and released before this replica contended left an acquisition that looked uncontended, and its fresh credential was redeemed a second time. Every acquisition now runs one rotation check, which also collapses two code paths into one. `getTokens` already loads the refresh record to decide a refresh is needed, so it is passed on as the observation baseline instead of read again, and the read taken under the flight is reused as the credential redeemed. Two reads on a latency-counted path where there were three. `runSilentRefresh` collapsed contention into null, sending the 401 path to interactive OAuth, so a slow peer prompted the user to authorize a server whose credential was about to be valid. The retryable outcome now travels through the silent-refresh layers and the connection defers, matched by name as well as identity because these errors cross the package boundary. * chore: Regenerate the Agents OpenAPI spec for repositoryInstructions The drift check added by #15928 fails on dev: #16008 added `repositoryInstructions` to the agent schema without regenerating the committed spec, so every branch merging dev inherits the failure. This is the remedy the check itself prescribes and carries no hand-written change. Drop this commit if dev regenerates first. * fix: preserve MCP refresh outcomes across replica boundaries * fix: keep MCP adoption fenced through publication * fix: anchor MCP refresh adoption to rejected credentials * fix: Complete OAuth Adoption Adapters and Isolate Legacy Flow Readers * fix: Preserve OAuth Request Identity Through Recovery and Discovery * fix: Fence OAuth Adoption Against All Credential Writers * fix: Gate Coordinated OAuth Rollout and Preserve Unauthenticated Identity * fix: Preserve OAuth Coalescing Across the Staged Rollout * style: Sort OAuth Timeout Constant Import * fix: Invalidate Both OAuth Token Flow Protocols on Rotation * test: Complete the Rollback Token Flow Fixture --------- Co-authored-by: Lia <lia@librechat.ai> Co-authored-by: Danny Avila <danny@librechat.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )