diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index 05d1b7e714f..950efff0026 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -16,6 +16,7 @@ const { getLangfuseTraceMessageFields, isContentFilterError, assertModelBoundProviderContent, + reportLocatorTraversalFailure, collectModelBoundHistoricalFileIdState, projectModelBoundSourceFiles, isModelBoundAttachmentFile, @@ -302,6 +303,7 @@ class BaseClient { : [{ role: 'user', content: payload, isCreatedByUser: true, isUserSubmitted: true }]; const fileProjection = this.getModelBoundFileProjection(); assertModelBoundProviderContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: this.options.req?.config?.filters, legacyPii: this.options.req?.config?.messageFilter?.pii, providerMessages: messages, diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 10f30569d13..58fc1824d77 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -145,6 +145,7 @@ const { decrementPendingRequest, maybePrewarmCodeSandbox, assertModelBoundContent, + reportLocatorTraversalFailure, filterFilesByEndpointRuntimeConfig, createModelBoundChatModelCallback: createModelBoundContentCallback, createInitialModelBoundAdmissionCallback, @@ -516,6 +517,7 @@ class AgentClient extends BaseClient { admitSteerAttachments(files, steerId) { const modelBoundFiles = files.filter(isModelBoundAttachmentFile); assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: this.options.req?.config?.filters, files: modelBoundFiles, }); @@ -2047,6 +2049,7 @@ class AgentClient extends BaseClient { return; } assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, legacyPii, storedMessages: this.modelBoundStoredMessages, }); @@ -2063,6 +2066,7 @@ class AgentClient extends BaseClient { const persistence = BaseClient.prototype.getModelBoundUserMessagePersistence.call(this); return createModelBoundContentCallback( { + onTraversalFailure: reportLocatorTraversalFailure, filters: this.options.req?.config?.filters, legacyPii: this.options.req?.config?.messageFilter?.pii, storedMessages: this.modelBoundStoredMessages, @@ -2393,6 +2397,7 @@ class AgentClient extends BaseClient { ]); void earlySharedContextPromise.catch(() => {}); assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: this.options.req.config?.filters, legacyPii: this.options.req.config?.messageFilter?.pii, agents: allAgents.map(({ agent }) => agent), @@ -2473,6 +2478,7 @@ class AgentClient extends BaseClient { this.modelBoundCurrentFiles = [...modelBoundRequestAttachments]; assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: this.options.req.config?.filters, files: modelBoundRequestAttachments, }); @@ -2720,6 +2726,7 @@ class AgentClient extends BaseClient { * user payload so strict file policy cannot be skipped by a late media * adapter. */ assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: this.options.req.config?.filters, legacyPii: this.options.req.config?.messageFilter?.pii, submittedMessages: [{ role: 'user', content: latestFormatted.content }], @@ -3013,6 +3020,7 @@ class AgentClient extends BaseClient { }); if (assertLateBoundContent) { assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: this.options.req.config?.filters, legacyPii: this.options.req.config?.messageFilter?.pii, agents: [agent], @@ -3041,6 +3049,7 @@ class AgentClient extends BaseClient { this.modelBoundMemoryContexts = [...modelBoundMemoryContexts]; this.modelBoundFileContexts = [...modelBoundFileContexts]; assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: this.options.req.config?.filters, legacyPii: this.options.req.config?.messageFilter?.pii, agents: allAgents.map(({ agent }) => agent), @@ -4676,6 +4685,7 @@ class AgentClient extends BaseClient { } assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: appConfig?.filters, legacyPii: appConfig?.messageFilter?.pii, agents: reachableAgents, @@ -5346,6 +5356,7 @@ class AgentClient extends BaseClient { }, { getAgentCheckpointer, + onTraversalFailure: reportLocatorTraversalFailure, getMessages: db.getMessages, getFiles: db.getFiles, }, @@ -5433,6 +5444,7 @@ class AgentClient extends BaseClient { agent === this.options.agent ? this.options.req.body.ephemeralAgent : undefined, }); assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: this.options.req.config?.filters, legacyPii: this.options.req.config?.messageFilter?.pii, agents: [agent], @@ -5531,6 +5543,7 @@ class AgentClient extends BaseClient { sharedRunContext: scopedContext ?? '', }); assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: this.options.req.config?.filters, legacyPii: this.options.req.config?.messageFilter?.pii, agents: [agent], diff --git a/api/server/controllers/agents/openai.js b/api/server/controllers/agents/openai.js index 78f930753f5..1ff29ca8744 100644 --- a/api/server/controllers/agents/openai.js +++ b/api/server/controllers/agents/openai.js @@ -50,6 +50,7 @@ const { isContentTraversalProtected, isContentTraversalLimitError, assertModelBoundContent, + reportLocatorTraversalFailure, hasModelBoundContentProtection, isContentFilterError, getSafeErrorMetadata, @@ -750,6 +751,7 @@ const executeOpenAIChatCompletion = async (envelope, { req, res }) => { const manualSkillPrimes = primaryConfig.manualSkillPrimes; const alwaysApplySkillPrimes = primaryConfig.alwaysApplySkillPrimes; assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: appConfig?.filters, legacyPii: appConfig?.messageFilter?.pii, submittedMessages: request.messages, diff --git a/api/server/controllers/agents/responses.js b/api/server/controllers/agents/responses.js index aa851fbb510..bccf4d3ddec 100644 --- a/api/server/controllers/agents/responses.js +++ b/api/server/controllers/agents/responses.js @@ -50,6 +50,7 @@ const { isContentTraversalLimitError, prependContentTraversalFragments, assertModelBoundContent, + reportLocatorTraversalFailure, hasModelBoundContentProtection, isContentFilterError, getSafeErrorMetadata, @@ -710,6 +711,7 @@ const executeResponse = async (envelope, { req, res }) => { : []; if (request.previous_response_id) { assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: appConfig?.filters, legacyPii: appConfig?.messageFilter?.pii, storedMessages: previousMessages, @@ -1000,6 +1002,7 @@ const executeResponse = async (envelope, { req, res }) => { const modelBoundAgents = [...modelBoundAgentsById.values()]; const mergedMCPAuthMap = discoveredMCPAuthMap ?? primaryConfig.userMCPAuthMap; assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: appConfig?.filters, legacyPii: appConfig?.messageFilter?.pii, agents: modelBoundAgents, @@ -1094,6 +1097,7 @@ const executeResponse = async (envelope, { req, res }) => { } assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: appConfig?.filters, legacyPii: appConfig?.messageFilter?.pii, submittedMessages: inputMessages, diff --git a/api/server/controllers/agents/resume.js b/api/server/controllers/agents/resume.js index 3b55b714abb..793e89568b8 100644 --- a/api/server/controllers/agents/resume.js +++ b/api/server/controllers/agents/resume.js @@ -31,6 +31,7 @@ const { getAgentCheckpointer, isContentFilterError, preflightResumeContent, + reportLocatorTraversalFailure, getResumeProvenance, getUserFacingResumeError, decrementPendingRequest, @@ -197,6 +198,7 @@ async function deleteFailedResumeCheckpoint(args, context) { const GENERIC_RESUME_ERROR = 'Resume failed'; const resumeContentProtectionDependencies = { + onTraversalFailure: reportLocatorTraversalFailure, getAgentCheckpointer, checkAccess, getMessages, diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 2d55ff44cda..f728a5bd4d2 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -37,6 +37,7 @@ const { isContentTraversalProtected, isContentTraversalLimitError, resolveCanonicalFileReferences, + reportLocatorTraversalFailure, } = require('@librechat/api'); const { Time, @@ -176,6 +177,7 @@ const blockFilteredAgentContent = async (req, res, agentData) => { if (filePolicyActive) { try { const fileInspection = await resolveCanonicalFileReferences({ + onTraversalFailure: reportLocatorTraversalFailure, filters, input: agentData, user: req.user, diff --git a/api/server/controllers/assistants/chatV1.js b/api/server/controllers/assistants/chatV1.js index cac475dee6b..5b33daa7950 100644 --- a/api/server/controllers/assistants/chatV1.js +++ b/api/server/controllers/assistants/chatV1.js @@ -12,6 +12,7 @@ const { isContentFilterError, hasActiveFilePolicy, preflightAssistantRunContent, + reportLocatorTraversalFailure, preflightAssistantUserMessageContent, } = require('@librechat/api'); const { @@ -335,6 +336,7 @@ const chatV1 = async (req, res) => { let persistedAssistant; try { persistedAssistant = await preflightAssistantRunContent({ + onTraversalFailure: reportLocatorTraversalFailure, config: req.config, openai, user: req.user, @@ -546,6 +548,7 @@ const chatV1 = async (req, res) => { await getRequestFileIds(); try { await preflightAssistantUserMessageContent({ + onTraversalFailure: reportLocatorTraversalFailure, config: req.config, user: req.user, message: userMessage, @@ -651,6 +654,7 @@ const chatV1 = async (req, res) => { try { await preflightAssistantRunContent({ + onTraversalFailure: reportLocatorTraversalFailure, config: req.config, openai, user: req.user, diff --git a/api/server/controllers/assistants/chatV2.js b/api/server/controllers/assistants/chatV2.js index ac451fa1732..b79053ba2ad 100644 --- a/api/server/controllers/assistants/chatV2.js +++ b/api/server/controllers/assistants/chatV2.js @@ -12,6 +12,7 @@ const { isContentFilterError, hasActiveFilePolicy, preflightAssistantRunContent, + reportLocatorTraversalFailure, preflightAssistantUserMessageContent, } = require('@librechat/api'); const { @@ -206,6 +207,7 @@ const chatV2 = async (req, res) => { await validateAuthor({ req, openai }); try { await preflightAssistantRunContent({ + onTraversalFailure: reportLocatorTraversalFailure, config: req.config, openai, user: req.user, @@ -377,6 +379,7 @@ const chatV2 = async (req, res) => { await getRequestFileIds(); try { await preflightAssistantUserMessageContent({ + onTraversalFailure: reportLocatorTraversalFailure, config: req.config, user: req.user, message: userMessage, @@ -489,6 +492,7 @@ const chatV2 = async (req, res) => { try { await preflightAssistantRunContent({ + onTraversalFailure: reportLocatorTraversalFailure, config: req.config, openai, user: req.user, diff --git a/api/server/routes/agents/chat.js b/api/server/routes/agents/chat.js index b9a16e560da..16ecf54fd04 100644 --- a/api/server/routes/agents/chat.js +++ b/api/server/routes/agents/chat.js @@ -2,6 +2,7 @@ const express = require('express'); const { logger } = require('@librechat/data-schemas'); const { createMessageFilterPii, + reportLocatorTraversalFailure, generateCheckAccess, skipAgentCheck, applyResumeContext, @@ -72,6 +73,7 @@ const restoreResumeContext = async (req, res, next) => { router.use(restoreResumeContext); router.use( createMessageFilterPii({ + onTraversalFailure: reportLocatorTraversalFailure, getConfig: (req) => req.config?.messageFilter?.pii, getFilters: (req) => req.config?.filters, getFiles, diff --git a/api/server/routes/agents/index.js b/api/server/routes/agents/index.js index 54405414737..afb425e7d80 100644 --- a/api/server/routes/agents/index.js +++ b/api/server/routes/agents/index.js @@ -1,5 +1,6 @@ const express = require('express'); const { + reportLocatorTraversalFailure, isEnabled, GenerationJobManager, TERMINAL_PUBLICATION_RECONNECT_ERROR, @@ -1054,6 +1055,7 @@ router.post( configMiddleware, ...steerLimiters, createMessageFilterPii({ + onTraversalFailure: reportLocatorTraversalFailure, getConfig: (req) => req.config?.messageFilter?.pii, getFilters: (req) => req.config?.filters, getFiles, @@ -1074,6 +1076,7 @@ router.post( configMiddleware, ...steerLimiters, createMessageFilterPii({ + onTraversalFailure: reportLocatorTraversalFailure, getConfig: (req) => req.config?.messageFilter?.pii, getFilters: (req) => req.config?.filters, getFiles, @@ -1113,6 +1116,7 @@ router.post( configMiddleware, ...steerLimiters, createMessageFilterPii({ + onTraversalFailure: reportLocatorTraversalFailure, getConfig: (req) => req.config?.messageFilter?.pii, getFilters: (req) => req.config?.filters, getFiles, diff --git a/api/server/routes/agents/openai.js b/api/server/routes/agents/openai.js index cf2cbb6e9ae..bb5bb83478b 100644 --- a/api/server/routes/agents/openai.js +++ b/api/server/routes/agents/openai.js @@ -20,6 +20,7 @@ */ const express = require('express'); const { + reportLocatorTraversalFailure, createAgentEventBindingHandlers, createAgentTriggerIngressHandlers, createMessageFilterPii, @@ -82,7 +83,10 @@ router.post( router.post( '/events', agentEventUserLimiter, - createMessageFilterPii({ getConfig: (req) => req.config?.messageFilter?.pii }), + createMessageFilterPii({ + onTraversalFailure: reportLocatorTraversalFailure, + getConfig: (req) => req.config?.messageFilter?.pii, + }), eventBindingHandlers.resolve, checkAgentTriggerPermission, eventHandlers.enqueueEvent, diff --git a/api/server/routes/agents/tools.js b/api/server/routes/agents/tools.js index f1d2c7c2703..843e3a56f26 100644 --- a/api/server/routes/agents/tools.js +++ b/api/server/routes/agents/tools.js @@ -1,11 +1,16 @@ const express = require('express'); -const { createContentFilter, extractToolArgumentContent } = require('@librechat/api'); +const { + reportLocatorTraversalFailure, + createContentFilter, + extractToolArgumentContent, +} = require('@librechat/api'); const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools'); const { getAvailableTools } = require('~/server/controllers/PluginController'); const { toolCallLimiter } = require('~/server/middleware'); const router = express.Router(); const filterToolArguments = createContentFilter({ + onTraversalFailure: reportLocatorTraversalFailure, getFilters: (req) => req.config?.filters, extract: (req) => { const { diff --git a/api/server/routes/assistants/chatV1.js b/api/server/routes/assistants/chatV1.js index 245b61aee8a..46820cde569 100644 --- a/api/server/routes/assistants/chatV1.js +++ b/api/server/routes/assistants/chatV1.js @@ -1,5 +1,5 @@ const express = require('express'); -const { createMessageFilterPii } = require('@librechat/api'); +const { createMessageFilterPii, reportLocatorTraversalFailure } = require('@librechat/api'); const router = express.Router(); const { handleAbort, validateModel, buildEndpointOption } = require('~/server/middleware'); @@ -12,6 +12,7 @@ const { getFiles } = require('~/models'); router.post('/abort', handleAbort()); const filterMessageContent = createMessageFilterPii({ + onTraversalFailure: reportLocatorTraversalFailure, getConfig: (req) => req.config?.messageFilter?.pii, getFilters: (req) => req.config?.filters, getFiles, diff --git a/api/server/routes/assistants/chatV2.js b/api/server/routes/assistants/chatV2.js index 2f28dac0c82..6e105539a99 100644 --- a/api/server/routes/assistants/chatV2.js +++ b/api/server/routes/assistants/chatV2.js @@ -1,5 +1,5 @@ const express = require('express'); -const { createMessageFilterPii } = require('@librechat/api'); +const { createMessageFilterPii, reportLocatorTraversalFailure } = require('@librechat/api'); const router = express.Router(); const { handleAbort, validateModel, buildEndpointOption } = require('~/server/middleware'); @@ -12,6 +12,7 @@ const { getFiles } = require('~/models'); router.post('/abort', handleAbort()); const filterMessageContent = createMessageFilterPii({ + onTraversalFailure: reportLocatorTraversalFailure, getConfig: (req) => req.config?.messageFilter?.pii, getFilters: (req) => req.config?.filters, getFiles, diff --git a/api/server/routes/assistants/v1.js b/api/server/routes/assistants/v1.js index 63d79a44f2f..d82a2f24314 100644 --- a/api/server/routes/assistants/v1.js +++ b/api/server/routes/assistants/v1.js @@ -1,5 +1,9 @@ const express = require('express'); -const { createContentFilter, extractAssistantContent } = require('@librechat/api'); +const { + reportLocatorTraversalFailure, + createContentFilter, + extractAssistantContent, +} = require('@librechat/api'); const controllers = require('~/server/controllers/assistants/v1'); const { getFiles } = require('~/models'); const documents = require('./documents'); @@ -9,6 +13,7 @@ const tools = require('./tools'); const router = express.Router(); const avatar = express.Router(); const filterAssistantContent = createContentFilter({ + onTraversalFailure: reportLocatorTraversalFailure, getFilters: (req) => req.config?.filters, extract: (req) => extractAssistantContent(req.body), getOpaqueFileInput: (req) => req.body, diff --git a/api/server/routes/assistants/v2.js b/api/server/routes/assistants/v2.js index 748b123e0a5..4af1dea42af 100644 --- a/api/server/routes/assistants/v2.js +++ b/api/server/routes/assistants/v2.js @@ -1,5 +1,9 @@ const express = require('express'); -const { createContentFilter, extractAssistantContent } = require('@librechat/api'); +const { + reportLocatorTraversalFailure, + createContentFilter, + extractAssistantContent, +} = require('@librechat/api'); const { configMiddleware } = require('~/server/middleware'); const v1 = require('~/server/controllers/assistants/v1'); const v2 = require('~/server/controllers/assistants/v2'); @@ -11,6 +15,7 @@ const tools = require('./tools'); const router = express.Router(); router.use(configMiddleware); const filterAssistantContent = createContentFilter({ + onTraversalFailure: reportLocatorTraversalFailure, getFilters: (req) => req.config?.filters, extract: (req) => extractAssistantContent(req.body), getOpaqueFileInput: (req) => req.body, diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index bdc00fb5f86..04c9ec5d992 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -2,6 +2,7 @@ const multer = require('multer'); const express = require('express'); const { sleep } = require('@librechat/agents'); const { + reportLocatorTraversalFailure, isEnabled, normalizeLimit, openCheckpointDeletion, @@ -64,10 +65,12 @@ const parentSubagentIndexHandler = createParentSubagentIndexHandler({ listSubagentTasksForThreads: db.listSubagentTasksForThreads, }); const filterConversationTitle = createContentFilter({ + onTraversalFailure: reportLocatorTraversalFailure, getFilters: (req) => req.config?.filters, extract: (req) => extractConversationTitleContent(req.body), }); const filterSubagentControlMessage = createContentFilter({ + onTraversalFailure: reportLocatorTraversalFailure, getFilters: (req) => req.config?.filters, getLegacyPii: (req) => req.config?.messageFilter?.pii, extract: (req) => diff --git a/api/server/routes/messages.js b/api/server/routes/messages.js index 24704ba7695..756f8ff8cb1 100644 --- a/api/server/routes/messages.js +++ b/api/server/routes/messages.js @@ -22,6 +22,7 @@ const { assertStoredMessageMutationAllowed, assertChatMutationAllowed, assertStoredMessageBranchAllowed, + reportLocatorTraversalFailure, mergeUserSubmittedPaths, mergeUserSubmittedMessageFieldPaths, isContentFilterError, @@ -40,6 +41,8 @@ const db = require('~/models'); const router = express.Router(); const filterStoredMessageContent = createContentFilter({ + messageCount: 1, + onTraversalFailure: reportLocatorTraversalFailure, getFilters: (req) => req.config?.filters, getMessageRoles: (req) => [req.body?.role], getOpaqueFileInput: (req) => req.body, @@ -47,6 +50,7 @@ const filterStoredMessageContent = createContentFilter({ extract: (req) => extractStoredMessageContent(req.body), }); const filterFeedbackContent = createContentFilter({ + onTraversalFailure: reportLocatorTraversalFailure, getFilters: (req) => req.config?.filters, extract: (req) => extractFeedbackContent(req.body), }); @@ -351,7 +355,7 @@ router.post('/branch', configMiddleware, async (req, res) => { message: newMessage, user: req.user, }, - { getFiles: db.getFiles }, + { getFiles: db.getFiles, onTraversalFailure: reportLocatorTraversalFailure }, ); const savedMessage = await db.saveMessage( diff --git a/api/server/routes/presets.js b/api/server/routes/presets.js index 7e3ee1b92e2..35b5e697ab6 100644 --- a/api/server/routes/presets.js +++ b/api/server/routes/presets.js @@ -2,6 +2,7 @@ const crypto = require('crypto'); const express = require('express'); const { logger } = require('@librechat/data-schemas'); const { + reportLocatorTraversalFailure, createContentFilter, extractPresetContent, projectStoredPresets, @@ -11,6 +12,7 @@ const { requireJwtAuth, configMiddleware } = require('~/server/middleware'); const router = express.Router(); const filterPresetContent = createContentFilter({ + onTraversalFailure: reportLocatorTraversalFailure, getFilters: (req) => req.config?.filters, extract: (req) => extractPresetContent(req.body), }); diff --git a/api/server/routes/share.js b/api/server/routes/share.js index 2aad568c247..3cd5761cb19 100644 --- a/api/server/routes/share.js +++ b/api/server/routes/share.js @@ -3,6 +3,7 @@ const express = require('express'); const { assertModelBoundContent, createShareContentPreflight, + reportLocatorTraversalFailure, isEnabled, isContentFilterError, isConversationImportError, @@ -140,6 +141,7 @@ const PREVIEW_LAZY_SWEEP_CUTOFF_MS = 2 * 60 * 1000; const enforceSharedFileContentPolicy = (req, res, next) => { try { assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters: req.config?.filters, files: [req.liveFile], }); @@ -367,6 +369,7 @@ if (allowSharedLinks) { async (req, res) => { try { const contentPreflight = createShareContentPreflight(req.config?.filters, { + onTraversalFailure: reportLocatorTraversalFailure, sharedFileMetadata: true, legacyPii: req.config?.messageFilter?.pii, }); @@ -429,6 +432,7 @@ if (allowSharedLinks) { // the GET share route so disabled file snapshots aren't copied into forks. snapshotFiles: !isFileSnapshotKillSwitchActive(), sharedContentPreflight: createShareContentPreflight(req.config?.filters, { + onTraversalFailure: reportLocatorTraversalFailure, sharedFileMetadata: true, legacyPii: req.config?.messageFilter?.pii, }), @@ -653,6 +657,7 @@ router.post( // did not uncheck "share files" (body flag absent defaults to enabled). const snapshotFiles = isFileSnapshotEnabled(req.config) && requestedSnapshotFiles !== false; const contentPreflight = createShareContentPreflight(req.config?.filters, { + onTraversalFailure: reportLocatorTraversalFailure, snapshotFiles, user: req.user, getFiles, @@ -723,6 +728,7 @@ router.patch( const snapshotFiles = isFileSnapshotEnabled(req.config) && requestedSnapshotFiles !== false; const contentPreflight = createShareContentPreflight(req.config?.filters, { + onTraversalFailure: reportLocatorTraversalFailure, snapshotFiles, user: req.user, getFiles, diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 88dae404c56..bc7693cf34a 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -32,6 +32,7 @@ const { inspectContentWithTraversal, ContentFilterError, assertModelBoundContent, + reportLocatorTraversalFailure, extractToolArgumentContent, contentFilterModelBoundBlockResponse, getSafeErrorMetadata, @@ -183,6 +184,7 @@ const assertToolResourcesAllowed = ({ req, toolResources, tools }) => { Array.isArray(resource?.files) ? resource.files : [], ); assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters, agents: [{ tool_resources: activeResources }], files, @@ -200,6 +202,7 @@ const withoutEncryptedActionSecrets = (action) => { const prepareStoredActionsForUse = async ({ actions, filters, decrypt }) => { if (filters != null) { assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, filters, actions: actions.map(withoutEncryptedActionSecrets), }); @@ -220,7 +223,11 @@ const prepareStoredActionsForUse = async ({ actions, filters, decrypt }) => { })), ); if (filters != null) { - assertModelBoundContent({ filters, actions: decryptedActions }); + assertModelBoundContent({ + onTraversalFailure: reportLocatorTraversalFailure, + filters, + actions: decryptedActions, + }); } return decryptedActions; }; diff --git a/api/server/utils/import/importBatchBuilder.js b/api/server/utils/import/importBatchBuilder.js index ac8da31cfa5..afbd5d3281e 100644 --- a/api/server/utils/import/importBatchBuilder.js +++ b/api/server/utils/import/importBatchBuilder.js @@ -3,6 +3,7 @@ const { assertConversationImportWriteSize, assertModelBoundContent, assertConversationImportContentAllowed, + reportLocatorTraversalFailure, executeConversationImportWrites, } = require('@librechat/api'); const { @@ -56,6 +57,7 @@ function createImportBatchBuilder(requestUserId, interfaceConfig, filters, legac async function assertConversationContentAllowed(filters, snapshot, resolutionContext = {}) { return assertConversationImportContentAllowed(filters, snapshot, { ...resolutionContext, + onTraversalFailure: reportLocatorTraversalFailure, assertModelBoundContent, }); } diff --git a/api/server/utils/import/importBatchBuilder.spec.js b/api/server/utils/import/importBatchBuilder.spec.js index f14ad5fd498..f7a28f54d03 100644 --- a/api/server/utils/import/importBatchBuilder.spec.js +++ b/api/server/utils/import/importBatchBuilder.spec.js @@ -437,8 +437,21 @@ describe('ImportBatchBuilder content filtering', () => { const messageCalls = mockAssertModelBoundContent.mock.calls.filter( ([input]) => input.storedMessages != null, ); - expect(fileCalls).toEqual([[{ filters, resolvedFiles: [canonicalFile] }]]); + expect(fileCalls).toEqual([ + [ + { + filters, + resolvedFiles: [canonicalFile], + onTraversalFailure: actualApi.reportLocatorTraversalFailure, + }, + ], + ]); expect(messageCalls).toHaveLength(2); + expect( + messageCalls.every( + ([input]) => input.onTraversalFailure === actualApi.reportLocatorTraversalFailure, + ), + ).toBe(true); expect(messageCalls.every(([input]) => input.storedMessages.length === 1)).toBe(true); expect(messageCalls.every(([input]) => input.resolvedFiles == null)).toBe(true); }); @@ -742,8 +755,21 @@ describe('ImportBatchBuilder content filtering', () => { const messageCalls = mockAssertModelBoundContent.mock.calls.filter( ([input]) => input.storedMessages != null, ); - expect(fileCalls).toEqual([[{ filters, resolvedFiles: [canonicalFile] }]]); + expect(fileCalls).toEqual([ + [ + { + filters, + resolvedFiles: [canonicalFile], + onTraversalFailure: actualApi.reportLocatorTraversalFailure, + }, + ], + ]); expect(messageCalls).toHaveLength(2); + expect( + messageCalls.every( + ([input]) => input.onTraversalFailure === actualApi.reportLocatorTraversalFailure, + ), + ).toBe(true); expect(messageCalls.every(([input]) => input.storedMessages.length === 1)).toBe(true); expect(messageCalls.every(([input]) => input.resolvedFiles == null)).toBe(true); expect(bulkSaveMessages).not.toHaveBeenCalled(); diff --git a/client/package.json b/client/package.json index 669107f62bc..a8f070957c9 100644 --- a/client/package.json +++ b/client/package.json @@ -80,8 +80,8 @@ "js-cookie": "^3.0.5", "librechat-data-provider": "*", "lodash": "^4.17.23", - "lucide": "^0.525.0", - "lucide-react": "^0.525.0", + "lucide": "^0.528.0", + "lucide-react": "^0.528.0", "match-sorter": "^8.1.0", "mdast-util-directive": "^3.0.0", "mdast-util-from-markdown": "^2.0.1", diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx index 96b0ad732a8..de800d6eb4d 100644 --- a/client/src/components/Chat/ChatView.tsx +++ b/client/src/components/Chat/ChatView.tsx @@ -8,6 +8,7 @@ import { Constants, buildTree } from 'librechat-data-provider'; import type { TChatProject } from 'librechat-data-provider'; import type { ChatFormValues } from '~/common'; import { + useScrollbarGutterSeed, useAddedResponse, useResumeOnLoad, useAdaptiveSSE, @@ -20,13 +21,13 @@ import ApprovalProvider from './Messages/Content/ApprovalContext'; import ConversationStarters from './Input/ConversationStarters'; import { pendingApprovalActionFamily } from './approval/state'; import { useGetMessagesByConvoId } from '~/data-provider'; +import Footer, { useConfiguredFooter } from './Footer'; import { AskAnswerHostProvider } from './ask/state'; import MessagesView from './Messages/MessagesView'; import Presentation from './Presentation'; import ChatForm from './Input/ChatForm'; import Landing from './Landing'; import Header from './Header'; -import Footer from './Footer'; import { cn } from '~/utils'; import store from '~/store'; @@ -51,6 +52,15 @@ function ChatView({ index = 0, project }: { index?: number; project?: TChatProje pendingApprovalActionFamily(conversationId ?? Constants.NEW_CONVO), ); + /** The welcome screen reserves the message column's scrollbar band before any + * column exists to measure it (see the column's class list below). */ + useScrollbarGutterSeed(); + + /** A conversation carries a footer only for configured content, and the + * composer's clearance has to account for the bar when it does — including + * while the config is still in flight, so a cold load does not jump. */ + const configuredFooter = useConfiguredFooter(); + const methods = useForm({ defaultValues: { text: '' }, }); @@ -101,6 +111,12 @@ function ChatView({ index = 0, project }: { index?: number; project?: TChatProje const isLandingPage = (!messagesTree || messagesTree.length === 0) && (conversationId === Constants.NEW_CONVO || !conversationId); + + /** A footer bar renders beneath the composer on the welcome screen always, and + * in a conversation when the deployment configured one. `present` already + * carries the remembered answer while the config is in flight, so this is the + * same value before and after it resolves. */ + const footerBelow = isLandingPage || configuredFooter.present; const isNavigating = (!messagesTree || messagesTree.length === 0) && conversationId != null; const isProjectLandingPage = isLandingPage && project != null; @@ -151,7 +167,15 @@ function ChatView({ index = 0, project }: { index?: number; project?: TChatProje className={cn( 'flex flex-col', isLandingPage - ? 'flex-1 items-center justify-end sm:justify-center' + ? /* The gutter is reserved once per state, wherever the + centring happens. A conversation centres the composer + inside the band below, against a message column that + holds the scrollbar band back; the landing page centres + this whole column instead, greeting and composer + together, so it holds the same band back here. Without + it the composer lands 4px right of where a conversation + puts it and slides sideways on the way in. */ + 'scrollbar-gutter-spacer flex-1 items-center justify-end sm:justify-center' : 'h-full overflow-y-auto', )} > @@ -181,9 +205,15 @@ function ChatView({ index = 0, project }: { index?: number; project?: TChatProje index={index} placeholder={chatFormPlaceholder} project={isProjectLandingPage ? project : undefined} + isLandingPage={isLandingPage} + footerBelow={footerBelow} + centerFormOnLanding={centerFormOnLanding} /> )} - {!isLandingPage &&