mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-28 04:37:37 +00:00
🛂 feat: Filter Model-Bound Content by Source (#14425)
* feat: introduce optional content protection seam * feat: enforce source-aware content filters * feat: complete source-aware content enforcement * test: activate skill file-text fail-close fixtures * fix: harden source-aware content filters * fix: harden model-bound content filtering * fix: preserve legacy filters and generated files * fix: inspect shared scalar metadata * test: align mocks with current dev dependencies * feat: add persisted content filter safeguards * feat: complete source-aware content filter enforcement * fix: move resume content preflight into TypeScript * fix: close content inspection edge cases * fix: harden content protection boundaries * fix: complete content protection safeguards * test: align persisted memory filter coverage * fix: reconcile content protection with current dev * fix: reconcile content protection with latest dev * fix: close content protection review gaps * fix: enforce source-aware provider boundaries * fix: preserve legacy PII preflight semantics * test: stabilize stored branch preflight fixture * fix: defer agent writes until protected model admission * perf: harden source-aware model-bound filtering * fix: canonicalize provider lineage before validation * fix: satisfy model-bound callback type checks * perf: Bound content protection filtering work * fix: Bound submission array traversal * fix: Stabilize bounded content snapshots * fix: Scope model-bound traversal overflows * fix: Preserve scoped content inspection * fix: Accumulate aggregate traversal scopes * fix: centralize content policy boundaries * test: align deferred tool policy context * test: align controller policy mocks * style: normalize content protection imports * fix: close content policy review gaps * fix: narrow active skill policy config * fix: address content protection review boundaries * fix: retain exact provenance overflow sentinel * fix: preserve literal and scoped provenance updates * fix: narrow persisted edit provenance * fix: isolate exact overflow attribution * fix: centralize stored prompt protection * fix: fail closed on incomplete transcript evidence * fix: align canonical transcript routing * refactor: centralize content policy preflights * fix: isolate upload policy error typing * style: sort policy preflight imports * refactor: centralize content policy boundaries
This commit is contained in:
parent
10f95c0ce9
commit
67b7b441b2
303 changed files with 75546 additions and 2327 deletions
|
|
@ -81,7 +81,9 @@ function cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder) {
|
|||
* @param {boolean} [params.records=false] - Optional flag for returning actual database records or resulting conversation and messages.
|
||||
* @param {boolean} [params.splitAtTarget=false] - Optional flag for splitting the messages at the target message level.
|
||||
* @param {string} [params.latestMessageId] - latestMessageId - Required if splitAtTarget is true.
|
||||
* @param {(userId: string) => ImportBatchBuilder} [params.builderFactory] - Optional factory function for creating an ImportBatchBuilder instance.
|
||||
* @param {object} [params.filters] - Source-aware content filters applied before cloned records are persisted.
|
||||
* @param {object} [params.legacyPii] - Legacy messageFilter.pii applied before cloned records are persisted.
|
||||
* @param {(userId: string, interfaceConfig?: object, filters?: object, legacyPii?: object) => ImportBatchBuilder} [params.builderFactory] - Optional factory function for creating an ImportBatchBuilder instance.
|
||||
* @returns {Promise<TForkConvoResponse>} The response after forking the conversation.
|
||||
*/
|
||||
async function forkConversation({
|
||||
|
|
@ -93,6 +95,8 @@ async function forkConversation({
|
|||
records = false,
|
||||
splitAtTarget = false,
|
||||
latestMessageId,
|
||||
filters,
|
||||
legacyPii,
|
||||
builderFactory = createImportBatchBuilder,
|
||||
}) {
|
||||
try {
|
||||
|
|
@ -110,7 +114,10 @@ async function forkConversation({
|
|||
targetMessageId = latestMessageId;
|
||||
}
|
||||
|
||||
const importBatchBuilder = builderFactory(requestUserId);
|
||||
const importBatchBuilder =
|
||||
legacyPii == null
|
||||
? builderFactory(requestUserId, undefined, filters)
|
||||
: builderFactory(requestUserId, undefined, filters, legacyPii);
|
||||
importBatchBuilder.startConversation(originalConvo.endpoint ?? EModelEndpoint.openAI);
|
||||
|
||||
let messagesToClone = [];
|
||||
|
|
@ -400,7 +407,8 @@ function isSameRevision(storedUpdatedAt, clientRevision) {
|
|||
* @param {number} [params.targetMessageIndex] - Index, within the shared payload, of the message at the tip of the branch the viewer has active. When set, only the direct path to that message is cloned so the fork continues the branch that was actually shown rather than the newest sibling. An index is used (not id or `createdAt`) because shared ids are re-anonymized per request while `getSharedMessages` returns a deterministic, stable order, so the same index resolves to the same message on the server.
|
||||
* @param {string} [params.shareRevision] - `updatedAt` of the payload the viewer is forking from. A shareId now survives an update, so an owner republishing between the GET and the fork would silently shift `targetMessageIndex` onto a different branch; a mismatch is rejected instead of cloning content the viewer never saw.
|
||||
* @param {boolean} [params.snapshotFiles] - When `false`, file/attachment metadata is omitted from the cloned messages, mirroring the GET share route so the global shared-file kill switch is honored.
|
||||
* @param {(userId: string, interfaceConfig?: object) => ImportBatchBuilder} [params.builderFactory] - Optional factory function for creating an ImportBatchBuilder instance.
|
||||
* @param {(snapshot: object) => Promise<void>} [params.sharedContentPreflight] - Reapplies current policy to the exact public projection before a legacy shared-file snapshot is persisted.
|
||||
* @param {(userId: string, interfaceConfig?: object, filters?: object, legacyPii?: object) => ImportBatchBuilder} [params.builderFactory] - Optional factory function for creating an ImportBatchBuilder instance.
|
||||
* @param {(options: object) => Promise<object>} [params.loadAppConfig] - Resolves the app config; injectable for tests. Called inside the requesting user's tenant context so retention policy is read from the viewer's tenant, not the share owner's.
|
||||
* @returns {Promise<TForkConvoResponse | null>} The new conversation and messages, or null when the share is missing or empty.
|
||||
*/
|
||||
|
|
@ -413,13 +421,17 @@ async function forkSharedConversation({
|
|||
targetMessageIndex,
|
||||
shareRevision,
|
||||
snapshotFiles,
|
||||
sharedContentPreflight,
|
||||
builderFactory = createImportBatchBuilder,
|
||||
loadAppConfig = getAppConfig,
|
||||
}) {
|
||||
// Mirror the GET share route: when the shared-file snapshot is globally
|
||||
// disabled, omit file/attachment metadata so a fork can't persist filenames
|
||||
// or share file URLs into the new conversation while file serving is off.
|
||||
const share = await getSharedMessages(shareId, shareResourceId, { snapshotFiles });
|
||||
const share = await getSharedMessages(shareId, shareResourceId, {
|
||||
snapshotFiles,
|
||||
preflight: sharedContentPreflight,
|
||||
});
|
||||
if (!share?.messages?.length) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -494,16 +506,26 @@ async function forkSharedConversation({
|
|||
// can actually use; hard-coding OpenAI breaks the first follow-up message on
|
||||
// deployments that don't expose it.
|
||||
const { endpoint, model } = await resolveImportDefaultEndpoint({ requestUserId, userRole });
|
||||
const importBatchBuilder = builderFactory(requestUserId, appConfig?.interfaceConfig);
|
||||
const importBatchBuilder =
|
||||
appConfig?.messageFilter?.pii == null
|
||||
? builderFactory(requestUserId, appConfig?.interfaceConfig, appConfig?.filters)
|
||||
: builderFactory(
|
||||
requestUserId,
|
||||
appConfig?.interfaceConfig,
|
||||
appConfig?.filters,
|
||||
appConfig.messageFilter.pii,
|
||||
);
|
||||
importBatchBuilder.startConversation(endpoint);
|
||||
|
||||
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);
|
||||
|
||||
const result = importBatchBuilder.finishConversation(share.title, new Date(), {}, model);
|
||||
await importBatchBuilder.saveBatch();
|
||||
logger.debug(
|
||||
`user: ${requestUserId} | New conversation "${result.conversation.title}" forked from share ID ${shareId}`,
|
||||
);
|
||||
logger.debug('Shared conversation forked', {
|
||||
userId: requestUserId,
|
||||
shareId,
|
||||
conversationId: result.conversation.conversationId,
|
||||
});
|
||||
|
||||
const conversation = await getConvo(requestUserId, result.conversation.conversationId);
|
||||
const messages = await getMessages({
|
||||
|
|
@ -524,9 +546,19 @@ async function forkSharedConversation({
|
|||
* @param {string} params.userId - The ID of the user duplicating the conversation.
|
||||
* @param {string} params.conversationId - The ID of the conversation to duplicate.
|
||||
* @param {string} [params.title] - Optional title override for the duplicate.
|
||||
* @param {object} [params.filters] - Source-aware content filters applied before cloned records are persisted.
|
||||
* @param {object} [params.legacyPii] - Legacy messageFilter.pii applied before cloned records are persisted.
|
||||
* @param {(userId: string, interfaceConfig?: object, filters?: object, legacyPii?: object) => ImportBatchBuilder} [params.builderFactory] - Optional factory function for creating an ImportBatchBuilder instance.
|
||||
* @returns {Promise<{ conversation: TConversation, messages: TMessage[] }>} The duplicated conversation and messages.
|
||||
*/
|
||||
async function duplicateConversation({ userId, conversationId, title }) {
|
||||
async function duplicateConversation({
|
||||
userId,
|
||||
conversationId,
|
||||
title,
|
||||
filters,
|
||||
legacyPii,
|
||||
builderFactory = createImportBatchBuilder,
|
||||
}) {
|
||||
const originalConvo = await getConvo(userId, conversationId);
|
||||
if (!originalConvo) {
|
||||
throw new Error('Conversation not found');
|
||||
|
|
@ -542,7 +574,10 @@ async function duplicateConversation({ userId, conversationId, title }) {
|
|||
originalMessages[originalMessages.length - 1].messageId,
|
||||
);
|
||||
|
||||
const importBatchBuilder = createImportBatchBuilder(userId);
|
||||
const importBatchBuilder =
|
||||
legacyPii == null
|
||||
? builderFactory(userId, undefined, filters)
|
||||
: builderFactory(userId, undefined, filters, legacyPii);
|
||||
importBatchBuilder.startConversation(originalConvo.endpoint ?? EModelEndpoint.openAI);
|
||||
|
||||
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);
|
||||
|
|
@ -550,9 +585,12 @@ async function duplicateConversation({ userId, conversationId, title }) {
|
|||
const duplicateTitle = title || originalConvo.title;
|
||||
const result = importBatchBuilder.finishConversation(duplicateTitle, new Date(), originalConvo);
|
||||
await importBatchBuilder.saveBatch();
|
||||
logger.debug(
|
||||
`user: ${userId} | New conversation "${duplicateTitle}" duplicated from conversation ID ${conversationId}`,
|
||||
);
|
||||
logger.debug('Conversation duplicated', {
|
||||
userId,
|
||||
sourceConversationId: conversationId,
|
||||
conversationId: result.conversation.conversationId,
|
||||
hasTitleOverride: typeof title === 'string' && title.length > 0,
|
||||
});
|
||||
|
||||
const conversation = await getConvo(userId, result.conversation.conversationId);
|
||||
const messages = await getMessages({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue