🧾 fix: Harden Historical File Authorization (#13918)

* fix: Harden historical file authorization

* chore: Sort file authorization imports

* fix: Preserve authorized historical artifact refs

* chore: Format historical artifact hardening
This commit is contained in:
Danny Avila 2026-06-23 15:49:57 -04:00 committed by GitHub
parent 606292c5c5
commit 1eb460eb03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 755 additions and 74 deletions

View file

@ -809,10 +809,19 @@ describe('initializeAgent — attachment scoping', () => {
db,
);
expect(db.getToolFilesByIds).toHaveBeenCalledWith(
[toolFile.file_id],
new Set([EToolResources.file_search]),
{ userId: 'user-1', tenantId: undefined },
);
expect(db.updateFilesUsage).toHaveBeenNthCalledWith(1, [requestFile], undefined, {
user: 'user-1',
tenantId: undefined,
});
expect(db.updateFilesUsage).toHaveBeenNthCalledWith(2, [toolFile], undefined, {
user: 'user-1',
tenantId: undefined,
});
expect(db.updateFilesUsage).toHaveBeenNthCalledWith(2, [toolFile]);
});
});
@ -1992,13 +2001,17 @@ describe('initializeAgent — code-generated file thread filter (regression)', (
);
expect(getCodeGeneratedFiles).toHaveBeenCalledTimes(1);
expect(getCodeGeneratedFiles).toHaveBeenCalledWith('conv-1', [
'file-pptx-skill',
'file-output-csv',
]);
expect(getCodeGeneratedFiles).toHaveBeenCalledWith(
'conv-1',
['file-pptx-skill', 'file-output-csv'],
{ userId: 'user-1', tenantId: undefined },
);
/* Both functions now share the same primary anchor symmetric
* design that closes the sibling-branch hole. */
expect(getUserCodeFiles).toHaveBeenCalledWith(['file-pptx-skill', 'file-output-csv']);
expect(getUserCodeFiles).toHaveBeenCalledWith(['file-pptx-skill', 'file-output-csv'], {
userId: 'user-1',
tenantId: undefined,
});
});
it('selects messages.attachments alongside messages.files (regression)', async () => {
@ -2086,7 +2099,10 @@ describe('initializeAgent — code-generated file thread filter (regression)', (
{ ...db, getMessages, getCodeGeneratedFiles, getUserCodeFiles },
);
expect(getCodeGeneratedFiles).toHaveBeenCalledWith('conv-1', []);
expect(getCodeGeneratedFiles).toHaveBeenCalledWith('conv-1', [], {
userId: 'user-1',
tenantId: undefined,
});
/* `getUserCodeFiles` is gated on a non-empty array at the call site,
* so it shouldn't be invoked at all. `getCodeGeneratedFiles`'s own
* empty-guard is exercised by data-schemas tests. */

View file

@ -21,8 +21,8 @@ import type {
TUser,
} from 'librechat-data-provider';
import type { GenericTool, LCToolRegistry, ToolMap, LCTool } from '@librechat/agents';
import type { IMongoFile, FileOwnerScope } from '@librechat/data-schemas';
import type { Response as ServerResponse } from 'express';
import type { IMongoFile } from '@librechat/data-schemas';
import type {
ServerRequest,
EndpointDbMethods,
@ -411,22 +411,30 @@ export interface InitializeAgentDbMethods extends EndpointDbMethods {
updateFilesUsage: (
files: Array<{ file_id: string }>,
fileIds?: string[],
options?: { user?: string },
options?: { user?: string; tenantId?: string | null },
) => 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) */
filterFilesByAgentAccess?: TFilterFilesByAgentAccess;
/** Get tool files by IDs (user-uploaded files only, code files handled separately) */
getToolFilesByIds: (fileIds: string[], toolSet: Set<EToolResources>) => Promise<unknown[]>;
getToolFilesByIds: (
fileIds: string[],
toolSet: Set<EToolResources>,
ownerScope?: FileOwnerScope,
) => Promise<unknown[]>;
/** Get conversation file IDs */
getConvoFiles: (conversationId: string) => Promise<string[] | null>;
/** Get code-generated files by conversation ID and the file_ids
* referenced from messages in the current thread (collected via
* `messages.files[].file_id` during thread walk). */
getCodeGeneratedFiles?: (conversationId: string, threadFileIds?: string[]) => Promise<unknown[]>;
getCodeGeneratedFiles?: (
conversationId: string,
threadFileIds?: string[],
ownerScope?: FileOwnerScope,
) => Promise<unknown[]>;
/** Get user-uploaded execute_code files by file IDs (from message.files in thread) */
getUserCodeFiles?: (fileIds: string[]) => Promise<unknown[]>;
getUserCodeFiles?: (fileIds: string[], ownerScope: FileOwnerScope) => Promise<unknown[]>;
/** Get messages for a conversation (supports select for field projection) */
getMessages?: (
filter: { conversationId: string },
@ -563,6 +571,9 @@ export async function initializeAgent(
isInitialAgent = false,
} = params;
const requestFileOwnerId = req.user?.id;
const requestFileOwnerScope: FileOwnerScope | undefined = requestFileOwnerId
? { userId: requestFileOwnerId, tenantId: req.user?.tenantId }
: undefined;
if (!db) {
throw new Error('initializeAgent requires db methods to be passed');
@ -610,7 +621,13 @@ export async function initializeAgent(
}
}
const toolFiles = (await db.getToolFilesByIds(fileIds, toolResourceSet)) as IMongoFile[];
const toolFiles = requestFileOwnerScope
? ((await db.getToolFilesByIds(
fileIds,
toolResourceSet,
requestFileOwnerScope,
)) as IMongoFile[])
: [];
/**
* Retrieve execute_code files filtered to the current thread.
@ -651,14 +668,25 @@ export async function initializeAgent(
* which sibling first generated them see `getCodeGeneratedFiles`
* for the branched-conversation rationale. */
if (db.getCodeGeneratedFiles) {
codeGeneratedFiles = (await db.getCodeGeneratedFiles(
conversationId,
threadFileIds,
)) as IMongoFile[];
codeGeneratedFiles = requestFileOwnerScope
? ((await db.getCodeGeneratedFiles(
conversationId,
threadFileIds,
requestFileOwnerScope,
)) as IMongoFile[])
: [];
}
if (db.getUserCodeFiles && threadFileIds && threadFileIds.length > 0) {
userCodeFiles = (await db.getUserCodeFiles(threadFileIds)) as IMongoFile[];
if (
db.getUserCodeFiles &&
requestFileOwnerScope &&
threadFileIds &&
threadFileIds.length > 0
) {
userCodeFiles = (await db.getUserCodeFiles(
threadFileIds,
requestFileOwnerScope,
)) as IMongoFile[];
}
}
@ -668,21 +696,27 @@ export async function initializeAgent(
requestFiles.length && requestFileOwnerId
? ((await db.updateFilesUsage(requestFiles, undefined, {
user: requestFileOwnerId,
tenantId: req.user?.tenantId,
})) 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[])
: [];
let toolUsageFiles: IMongoFile[] = [];
if (trustedToolFiles.length > 0 && requestFileOwnerId) {
toolUsageFiles = (await db.updateFilesUsage(trustedToolFiles, undefined, {
user: requestFileOwnerId,
tenantId: req.user?.tenantId,
})) as IMongoFile[];
}
currentFiles = requestUsageFiles.concat(toolUsageFiles);
}
} else if (requestFiles.length) {
currentFiles = requestFileOwnerId
? ((await db.updateFilesUsage(requestFiles, undefined, {
user: requestFileOwnerId,
tenantId: req.user?.tenantId,
})) as IMongoFile[])
: [];
}