📎 fix: Scope Attachment Usage to Request Owner (#13557)

* fix: harden attachment usage handling

* fix: sort file method imports

* fix: clarify file usage scope
This commit is contained in:
Danny Avila 2026-06-06 14:23:04 -04:00 committed by GitHub
parent 3571dfcf22
commit 75bbefb1c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 184 additions and 12 deletions

View file

@ -33,7 +33,8 @@ jest.mock('@librechat/agents', () => ({
}));
import { Providers } from '@librechat/agents';
import { EModelEndpoint, Tools } from 'librechat-data-provider';
import { EModelEndpoint, EToolResources, Tools } from 'librechat-data-provider';
import type { IMongoFile } from '@librechat/data-schemas';
import type { Agent } from 'librechat-data-provider';
import type { ServerRequest, InitializeResultBase, EndpointTokenConfig } from '~/types';
import type { InitializeAgentDbMethods } from '../initialize';
@ -750,6 +751,44 @@ describe('initializeAgent — attachment scoping', () => {
expect(result.requestAttachments).toEqual([requestFile]);
expect(result.agentContextAttachments).toEqual([agentContextFile]);
});
it('owner-scopes request file usage updates while preserving trusted tool files', async () => {
const requestFile = { file_id: 'request-file', filename: 'request.txt' } as IMongoFile;
const toolFile = { file_id: 'tool-file', filename: 'tool.txt' } as IMongoFile;
const { agent, req, res, loadTools, db } = createMocks();
agent.tools = [EToolResources.file_search];
mockExtractLibreChatParams.mockReturnValueOnce({
resendFiles: true,
maxContextTokens: undefined,
modelOptions: { model: agent.model },
});
(db.getConvoFiles as jest.Mock).mockResolvedValueOnce([toolFile.file_id]);
(db.getToolFilesByIds as jest.Mock).mockResolvedValueOnce([toolFile]);
(db.updateFilesUsage as jest.Mock)
.mockResolvedValueOnce([requestFile])
.mockResolvedValueOnce([toolFile]);
await initializeAgent(
{
req,
res,
agent,
loadTools,
requestFiles: [requestFile],
conversationId: 'conversation-1',
endpointOption: { endpoint: EModelEndpoint.agents },
allowedProviders: new Set([Providers.OPENAI]),
isInitialAgent: true,
},
db,
);
expect(db.updateFilesUsage).toHaveBeenNthCalledWith(1, [requestFile], undefined, {
user: 'user-1',
});
expect(db.updateFilesUsage).toHaveBeenNthCalledWith(2, [toolFile]);
});
});
describe('initializeAgent — maxContextTokens', () => {

View file

@ -389,7 +389,11 @@ export interface InitializeAgentParams {
*/
export interface InitializeAgentDbMethods extends EndpointDbMethods {
/** Update usage tracking for multiple files */
updateFilesUsage: (files: Array<{ file_id: string }>, fileIds?: string[]) => Promise<unknown[]>;
updateFilesUsage: (
files: Array<{ file_id: string }>,
fileIds?: string[],
options?: { user?: string },
) => Promise<unknown[]>;
/** Get files from database */
getFiles: (filter: unknown, sort: unknown, select: unknown) => Promise<unknown[]>;
/** Filter files by agent access permissions (ownership or agent attachment) */
@ -539,6 +543,7 @@ export async function initializeAgent(
allowedProviders,
isInitialAgent = false,
} = params;
const requestFileOwnerId = req.user?.id;
if (!db) {
throw new Error('initializeAgent requires db methods to be passed');
@ -640,10 +645,27 @@ export async function initializeAgent(
const allToolFiles = toolFiles.concat(codeGeneratedFiles, userCodeFiles);
if (requestFiles.length || allToolFiles.length) {
currentFiles = (await db.updateFilesUsage(requestFiles.concat(allToolFiles))) as IMongoFile[];
const requestUsageFiles =
requestFiles.length && requestFileOwnerId
? ((await db.updateFilesUsage(requestFiles, undefined, {
user: requestFileOwnerId,
})) as IMongoFile[])
: [];
const requestUsageFileIds = new Set(requestUsageFiles.map((file) => file.file_id));
const trustedToolFiles = allToolFiles.filter(
(file) => !requestUsageFileIds.has(file.file_id),
);
const toolUsageFiles = trustedToolFiles.length
? ((await db.updateFilesUsage(trustedToolFiles)) as IMongoFile[])
: [];
currentFiles = requestUsageFiles.concat(toolUsageFiles);
}
} else if (requestFiles.length) {
currentFiles = (await db.updateFilesUsage(requestFiles)) as IMongoFile[];
currentFiles = requestFileOwnerId
? ((await db.updateFilesUsage(requestFiles, undefined, {
user: requestFileOwnerId,
})) as IMongoFile[])
: [];
}
if (currentFiles && currentFiles.length) {