Skip to content

[pull] main from danny-avila:main - #267

Merged
pull[bot] merged 17 commits into
innFactory:mainfrom
danny-avila:main
Sep 17, 2026
Merged

pull[bot] merged 17 commits into
innFactory:mainfrom
danny-avila:main

Conversation

@pull

@pull pull Bot commented Sep 17, 2026

Copy link
Copy Markdown

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 : )

danny-avila and others added 17 commits September 16, 2026 07:46
* 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
* 🏛️ 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>
@pull pull Bot locked and limited conversation to collaborators Sep 17, 2026
@pull pull Bot added the ⤵️ pull label Sep 17, 2026
@pull
pull Bot merged commit 2452f49 into innFactory:main Sep 17, 2026
1 check passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants