🛂 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:
Danny Avila 2026-08-21 22:43:32 -04:00 committed by GitHub
parent 10f95c0ce9
commit 67b7b441b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
303 changed files with 75546 additions and 2327 deletions

View file

@ -70,6 +70,17 @@ jest.mock('~/server/services/Files/Code/process', () => ({
},
}));
jest.mock('~/server/services/Files/Code/preflight', () => ({
preflightCodeOutputBatch: jest.fn(async ({ artifact }) =>
(artifact.files ?? [])
.filter((file) => file.inherited !== true)
.map((file) => ({
file,
sessionId: file.storage_session_id ?? artifact.session_id,
})),
),
}));
jest.mock('~/server/services/Tools/credentials', () => ({
loadAuthValues: jest.fn(),
}));
@ -436,6 +447,7 @@ describe('createToolEndCallback', () => {
* message slot, leaving the current turn's pending chip stuck. */
const { processCodeOutput } = require('~/server/services/Files/Code/process');
const { preflightCodeOutputBatch } = require('~/server/services/Files/Code/preflight');
function makeCodeExecutionEvent({
runId,
@ -867,6 +879,50 @@ describe('createToolEndCallback', () => {
expect(processCodeOutput).not.toHaveBeenCalled();
expect(res.write).not.toHaveBeenCalled();
});
it('rejects blocked generated bytes before queuing any persistence', async () => {
const blocked = new Error('Generated file content blocked');
preflightCodeOutputBatch.mockRejectedValueOnce(blocked);
const toolEndCallback = createToolEndCallback({ req, res, artifactPromises });
const event = makeCodeExecutionEvent({
runId: 'run-blocked',
threadId: 'thread-1',
toolCallId: 'tool-blocked',
fileId: 'fid-blocked',
name: 'blocked.txt',
});
await expect(toolEndCallback({ output: event.output }, event.metadata)).rejects.toBe(blocked);
expect(processCodeOutput).not.toHaveBeenCalled();
expect(artifactPromises).toHaveLength(0);
expect(res.write).not.toHaveBeenCalled();
});
it('rejects blocked generated bytes in the Responses callback before persistence', async () => {
const blocked = new Error('Generated file content blocked');
preflightCodeOutputBatch.mockRejectedValueOnce(blocked);
const { createResponsesToolEndCallback } = require('../callbacks');
const callback = createResponsesToolEndCallback({
req,
res,
tracker: { nextSequence: jest.fn(() => 1) },
artifactPromises,
});
const event = makeCodeExecutionEvent({
runId: 'run-responses-blocked',
threadId: 'thread-1',
toolCallId: 'tool-responses-blocked',
fileId: 'fid-responses-blocked',
name: 'blocked.txt',
});
await expect(callback({ output: event.output }, event.metadata)).rejects.toBe(blocked);
expect(processCodeOutput).not.toHaveBeenCalled();
expect(artifactPromises).toHaveLength(0);
expect(res.write).not.toHaveBeenCalled();
});
});
});

View file

@ -19,6 +19,43 @@ const mockBuildAgentContextAttachmentsByAgentId = jest.fn().mockReturnValue(new
const mockBuildInlineMemoryContext = jest.fn().mockResolvedValue('');
const mockApplyContextToAgent = jest.fn().mockResolvedValue(undefined);
const mockInitialSessions = new Map([['execute_code', { session_id: 'seeded' }]]);
const mockGetSafeErrorMetadata = jest.fn((error) => {
const status = error?.status ?? error?.statusCode ?? error?.response?.status;
return {
type: error instanceof Error ? 'Error' : 'UnknownError',
...(Number.isInteger(status) && status >= 100 && status <= 599 && { status }),
};
});
const mockHasActivePiiPatterns = (config) =>
config != null &&
(config.starterPatterns == null ||
config.starterPatterns.length > 0 ||
(config.customPatterns?.length ?? 0) > 0);
const mockHasModelBoundContentProtection = (filters, legacyPii) => {
const sourcePolicies = [
legacyPii,
filters?.messages?.pii,
filters?.agentInstructions?.pii,
filters?.conversationStarters?.pii,
filters?.skills?.pii,
filters?.memories?.pii,
filters?.files?.pii,
filters?.toolArguments?.pii,
filters?.modelParameters?.pii,
filters?.actionMetadata?.pii,
];
if (sourcePolicies.some(mockHasActivePiiPatterns)) {
return true;
}
const filePolicy = filters?.files?.pii;
return (
filePolicy?.uninspectable === 'block' &&
(filePolicy.fields == null ||
filePolicy.fields.some((field) =>
['content', 'extracted_text', 'transcript'].includes(field),
))
);
};
class MockAgentRunEnvelopeError extends TypeError {
constructor(message) {
super(message);
@ -107,6 +144,21 @@ jest.mock('@librechat/agents', () => ({
}));
jest.mock('@librechat/api', () => ({
collectReachableAgents: (roots) => {
const agents = [];
const pending = [...roots];
const visited = new Set();
for (let index = 0; index < pending.length; index++) {
const agent = pending[index];
if (!agent || visited.has(agent)) {
continue;
}
visited.add(agent);
agents.push(agent);
pending.push(...(agent.subagentAgentConfigs ?? []));
}
return agents;
},
/** Pass-through: the controller strips UI-only activity-label parts
* before SDK formatting; the mock must expose it like any other used
* export or the call throws before the assertions run. */
@ -184,7 +236,28 @@ jest.mock('@librechat/api', () => ({
resolveRecursionLimit: jest.fn().mockReturnValue(50),
createToolExecuteHandler: jest.fn().mockReturnValue({ handle: jest.fn() }),
isChatCompletionValidationFailure: jest.fn().mockReturnValue(false),
findPiiMatchInMessages: jest.fn().mockReturnValue(null),
inspectContent: jest.fn().mockReturnValue(null),
extractMessageContent: jest.fn().mockReturnValue([]),
extractModelParameterContent: jest.fn().mockReturnValue([]),
extractSkillContent: jest.fn().mockReturnValue([]),
getBlockedOpaqueFileField: jest.fn().mockReturnValue(null),
getContentTraversalFragments: jest.fn().mockReturnValue([]),
isContentTraversalProtected: jest.fn().mockReturnValue(true),
isContentTraversalLimitError: jest.fn((error) => error?.code === 'content_filter_uninspectable'),
assertModelBoundContent: jest.fn(),
hasModelBoundContentProtection: mockHasModelBoundContentProtection,
isContentFilterError: jest.fn((error) => error?.code === 'content_filter_block'),
getSafeErrorMetadata: mockGetSafeErrorMetadata,
contentFilterBlockResponse: jest.fn().mockReturnValue({
error: 'content_filter_block',
message: 'Submitted content was blocked.',
}),
contentFilterUninspectableResponse: jest.fn().mockReturnValue({
error: 'content_filter_uninspectable',
message: 'Submitted file content could not be inspected before processing.',
source: 'file',
field: 'content',
}),
discoverConnectedAgents: jest.fn().mockResolvedValue({
agentConfigs: new Map(),
edges: [],
@ -400,6 +473,507 @@ describe('OpenAIChatCompletionController', () => {
expect(aggregator.usage.completionTokens).toBe(initialCompletionTokens + 10);
});
describe('content filtering', () => {
it('blocks opaque inline media before text inspection or agent loading', async () => {
const api = require('@librechat/api');
const db = require('~/models');
const messages = [
{
role: 'user',
content: [
{
type: 'image_url',
image_url: { url: 'data:image/png;base64,do-not-echo' },
},
],
},
];
api.validateRequest.mockReturnValueOnce({
request: { model: 'agent-123', messages, stream: false },
});
api.getBlockedOpaqueFileField.mockReturnValueOnce('content');
await OpenAIChatCompletionController(req, res);
expect(api.getBlockedOpaqueFileField).toHaveBeenCalledWith(req.config.filters, messages);
expect(api.extractMessageContent).not.toHaveBeenCalled();
expect(db.getAgent).not.toHaveBeenCalled();
expect(api.createErrorResponse).toHaveBeenCalledWith(
'Submitted file content could not be inspected before processing.',
'invalid_request_error',
'content_filter_uninspectable',
);
expect(JSON.stringify(api.createErrorResponse.mock.calls)).not.toContain('do-not-echo');
});
it('returns a raw-free error when nested message inspection exhausts its budget', async () => {
const api = require('@librechat/api');
const db = require('~/models');
req.config.filters = { messages: { pii: { starterPatterns: [] } } };
api.extractMessageContent.mockImplementationOnce(() => {
throw {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted content could not be completely inspected before processing.',
source: 'message',
field: 'content_part',
},
};
});
await OpenAIChatCompletionController(req, res);
expect(db.getAgent).not.toHaveBeenCalled();
expect(api.createErrorResponse).toHaveBeenCalledWith(
'Submitted content could not be completely inspected before processing.',
'invalid_request_error',
'content_filter_uninspectable',
);
});
it('preserves field granularity when the exhausted nested field is not selected', async () => {
const api = require('@librechat/api');
const db = require('~/models');
req.config.filters = {
messages: { pii: { fields: ['text'], starterPatterns: [] } },
};
api.extractMessageContent.mockImplementationOnce(() => {
throw {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted content could not be completely inspected before processing.',
source: 'message',
field: 'content_part',
},
};
});
api.isContentTraversalProtected.mockReturnValueOnce(false);
await OpenAIChatCompletionController(req, res);
expect(db.getAgent).toHaveBeenCalled();
expect(api.createErrorResponse).not.toHaveBeenCalledWith(
expect.anything(),
'invalid_request_error',
'content_filter_uninspectable',
);
});
it('continues when exhausted model parameters are outside the active policy', async () => {
const api = require('@librechat/api');
const db = require('~/models');
api.extractModelParameterContent.mockImplementationOnce(() => {
throw {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted content could not be completely inspected before processing.',
source: 'model_parameter',
field: 'request_fields',
},
};
});
api.isContentTraversalProtected.mockReturnValueOnce(false);
await OpenAIChatCompletionController(req, res);
expect(db.getAgent).toHaveBeenCalled();
expect(api.createErrorResponse).not.toHaveBeenCalledWith(
expect.anything(),
'invalid_request_error',
'content_filter_uninspectable',
);
});
it('blocks submitted messages and model parameters before loading the agent', async () => {
const api = require('@librechat/api');
const db = require('~/models');
api.validateRequest.mockReturnValueOnce({
request: {
model: 'agent-123',
messages: [],
stream: false,
stop: ['submitted stop sequence'],
},
});
api.inspectContent.mockReturnValueOnce({
detectorId: 'pii-pattern',
ruleId: 'sk_prefix',
label: 'sk- prefix token',
source: 'model_parameter',
field: 'stop',
});
await OpenAIChatCompletionController(req, res);
expect(api.extractMessageContent).toHaveBeenCalled();
expect(api.extractModelParameterContent).toHaveBeenCalledWith(
expect.objectContaining({ stop: ['submitted stop sequence'] }),
);
expect(db.getAgent).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(api.createErrorResponse).toHaveBeenCalledWith(
'Submitted content was blocked.',
'invalid_request_error',
'content_filter_block',
);
});
it('blocks manually selected skill names before resolving the skill', async () => {
const api = require('@librechat/api');
const db = require('~/models');
req.body.manualSkills = ['PRIVATE-SKILL'];
api.extractManualSkills.mockReturnValueOnce(['PRIVATE-SKILL']);
api.inspectContent.mockReturnValueOnce({
detectorId: 'pii-pattern',
ruleId: 'private',
label: 'private value',
source: 'skill',
field: 'name',
});
await OpenAIChatCompletionController(req, res);
expect(api.extractSkillContent).toHaveBeenCalledWith({ name: 'PRIVATE-SKILL' });
expect(db.getAgent).not.toHaveBeenCalled();
expect(api.createErrorResponse).toHaveBeenCalledWith(
'Submitted content was blocked.',
'invalid_request_error',
'content_filter_block',
);
});
it('rejects filtered model-bound context before starting a streaming response', async () => {
const api = require('@librechat/api');
api.validateRequest.mockReturnValueOnce({
request: {
model: 'agent-123',
messages: [],
stream: true,
},
});
api.assertModelBoundContent.mockImplementationOnce(() => {
throw Object.assign(new Error('Submitted content contains a private value.'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content contains a private value. Remove it and try again.',
source: 'agent_instruction',
field: 'instructions',
},
});
});
await OpenAIChatCompletionController(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(api.createErrorResponse).toHaveBeenCalledWith(
'Submitted content contains a private value. Remove it and try again.',
'invalid_request_error',
'content_filter_block',
);
expect(res.setHeader).not.toHaveBeenCalled();
expect(res.flushHeaders).not.toHaveBeenCalled();
expect(api.writeSSE).not.toHaveBeenCalled();
expect(api.createRun).not.toHaveBeenCalled();
});
it('preflights file-derived content from every reachable agent under a files-only policy', async () => {
const api = require('@librechat/api');
const primaryRequestFile = { filename: 'primary-request.txt', content: 'primary request' };
const primaryContextFile = { filename: 'primary-context.txt', content: 'primary context' };
const handoffRequestFile = { filename: 'handoff-request.txt', content: 'handoff request' };
const handoffContextFile = {
filename: 'handoff-context.txt',
content: 'sk-handoff-context',
};
const nestedRequestFile = { filename: 'nested-request.txt', content: 'nested request' };
const nestedPureSubagent = {
id: 'agent-nested-pure',
model: 'gpt-4',
model_parameters: {},
requestAttachments: [nestedRequestFile],
dynamicToolContextMap: { nested_lookup: 'nested dynamic context' },
};
const pureSubagent = {
id: 'agent-pure',
model: 'gpt-4',
model_parameters: {},
subagentAgentConfigs: [nestedPureSubagent],
};
const blockedError = Object.assign(new Error('Submitted file content was blocked.'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted file content was blocked.',
source: 'file',
field: 'content',
},
});
req.config.filters = {
files: { pii: { fields: ['content'], starterPatterns: ['sk-'] } },
};
api.validateRequest.mockReturnValueOnce({
request: { model: 'agent-123', messages: [], stream: true },
});
api.initializeAgent.mockResolvedValueOnce({
id: 'agent-123',
model: 'gpt-4',
model_parameters: {},
toolRegistry: {},
edges: [{ source: 'agent-123', target: 'agent-handoff' }],
requestAttachments: [primaryRequestFile],
agentContextAttachments: [primaryContextFile],
dynamicToolContextMap: { execute_code: 'primary dynamic context' },
subagentAgentConfigs: [pureSubagent],
});
api.discoverConnectedAgents.mockResolvedValueOnce({
agentConfigs: new Map([
[
'agent-handoff',
{
id: 'agent-handoff',
model: 'gpt-4',
model_parameters: {},
requestAttachments: [handoffRequestFile],
agentContextAttachments: [handoffContextFile],
dynamicToolContextMap: { file_search: 'handoff dynamic context' },
},
],
]),
edges: [],
skippedAgentIds: new Set(),
userMCPAuthMap: undefined,
});
api.assertModelBoundContent.mockImplementationOnce(({ filters, agents, files }) => {
expect(filters).toEqual(req.config.filters);
expect(agents.map(({ id }) => id)).toEqual([
'agent-123',
'agent-handoff',
'agent-pure',
'agent-nested-pure',
]);
expect(files).toEqual([
primaryRequestFile,
primaryContextFile,
{ content: 'primary dynamic context' },
handoffRequestFile,
handoffContextFile,
{ content: 'handoff dynamic context' },
nestedRequestFile,
{ content: 'nested dynamic context' },
]);
throw blockedError;
});
await OpenAIChatCompletionController(req, res);
expect(api.createRun).not.toHaveBeenCalled();
expect(res.setHeader).not.toHaveBeenCalled();
expect(res.flushHeaders).not.toHaveBeenCalled();
expect(api.createErrorResponse).toHaveBeenCalledWith(
blockedError.body.message,
'invalid_request_error',
'content_filter_block',
);
});
it('preflights the exact synthesized dynamic tool context as file content', async () => {
const api = require('@librechat/api');
const blockedError = Object.assign(new Error('Submitted file content was blocked.'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted file content was blocked.',
source: 'file',
field: 'content',
},
});
req.config.filters = {
files: { pii: { fields: ['content'], starterPatterns: ['sk-'] } },
};
api.validateRequest.mockReturnValueOnce({
request: { model: 'agent-123', messages: [], stream: true },
});
api.initializeAgent.mockResolvedValueOnce({
id: 'agent-123',
model: 'gpt-4',
model_parameters: {},
toolRegistry: {},
edges: [],
dynamicToolContextMap: {
execute_code: ' safe context',
ignored_empty: '',
file_search: 'sk-dynamic-file-context ',
ignored_non_string: 42,
},
});
api.assertModelBoundContent.mockImplementationOnce(({ filters, files }) => {
expect(filters).toEqual(req.config.filters);
expect(files).toEqual([{ content: 'safe context\nsk-dynamic-file-context' }]);
throw blockedError;
});
await OpenAIChatCompletionController(req, res);
expect(api.createRun).not.toHaveBeenCalled();
expect(res.setHeader).not.toHaveBeenCalled();
expect(res.flushHeaders).not.toHaveBeenCalled();
expect(api.createErrorResponse).toHaveBeenCalledWith(
blockedError.body.message,
'invalid_request_error',
'content_filter_block',
);
});
});
describe('safe error logging', () => {
it('logs bounded metadata and returns a raw-free provider error', async () => {
const api = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const rawValue = 'PRIVATE-OPENAI-PROVIDER-PAYLOAD';
const providerError = Object.assign(new Error(`Provider echoed ${rawValue}`), {
code: 'ERR_REMOTE',
response: {
status: 502,
headers: { authorization: rawValue },
data: { prompt: rawValue },
},
});
req.config.filters = { messages: { pii: {} } };
mockProcessStream.mockRejectedValueOnce(providerError);
await OpenAIChatCompletionController(req, res);
expect(mockGetSafeErrorMetadata).toHaveBeenCalledWith(providerError);
const errorLog = logger.error.mock.calls.find(
([message]) => message === '[OpenAI API] Error:',
);
expect(errorLog).toEqual(['[OpenAI API] Error:', { type: 'Error', status: 502 }]);
expect(JSON.stringify(errorLog)).not.toContain(rawValue);
expect(api.createErrorResponse).toHaveBeenCalledWith(
'An error occurred while processing the request',
'server_error',
null,
);
expect(JSON.stringify(api.createErrorResponse.mock.calls)).not.toContain(rawValue);
expect(res.status).toHaveBeenCalledWith(500);
});
it('streams a raw-free provider error after headers are sent', async () => {
const api = require('@librechat/api');
const rawValue = 'PRIVATE-OPENAI-STREAM-PAYLOAD';
api.validateRequest.mockReturnValueOnce({
request: {
model: 'agent-123',
messages: [{ role: 'user', content: 'Hello' }],
stream: true,
},
});
req.config.filters = { messages: { pii: {} } };
res.flushHeaders.mockImplementationOnce(() => {
res.headersSent = true;
});
mockProcessStream.mockRejectedValueOnce(new Error(`Provider echoed ${rawValue}`));
await OpenAIChatCompletionController(req, res);
expect(api.createChunk).toHaveBeenCalledWith(
expect.any(Object),
{ content: '\n\nError: An error occurred while processing the request' },
'stop',
);
expect(JSON.stringify(api.createChunk.mock.calls)).not.toContain(rawValue);
expect(JSON.stringify(api.writeSSE.mock.calls)).not.toContain(rawValue);
});
it('preserves the legacy provider error when protection is inactive', async () => {
const api = require('@librechat/api');
const rawValue = 'LEGACY-OPENAI-PROVIDER-ERROR';
mockProcessStream.mockRejectedValueOnce(
Object.assign(new Error(rawValue), { code: 'ERR_LEGACY_REMOTE' }),
);
await OpenAIChatCompletionController(req, res);
expect(api.createErrorResponse).toHaveBeenCalledWith(
rawValue,
'server_error',
'ERR_LEGACY_REMOTE',
);
});
it.each([
['a management-only prompt', { prompts: { pii: {} } }],
['an inert message', { messages: { pii: { starterPatterns: [] } } }],
])('preserves the legacy provider error for %s policy', async (_policy, filters) => {
const api = require('@librechat/api');
const rawValue = 'LEGACY-OPENAI-CONFIGURED-PROVIDER-ERROR';
req.config.filters = filters;
mockProcessStream.mockRejectedValueOnce(new Error(rawValue));
await OpenAIChatCompletionController(req, res);
expect(api.createErrorResponse).toHaveBeenCalledWith(rawValue, 'server_error', null);
});
it('preserves the legacy streamed provider error when protection is inactive', async () => {
const api = require('@librechat/api');
const rawValue = 'LEGACY-OPENAI-STREAM-ERROR';
api.validateRequest.mockReturnValueOnce({
request: {
model: 'agent-123',
messages: [{ role: 'user', content: 'Hello' }],
stream: true,
},
});
res.flushHeaders.mockImplementationOnce(() => {
res.headersSent = true;
});
mockProcessStream.mockRejectedValueOnce(new Error(rawValue));
await OpenAIChatCompletionController(req, res);
expect(api.createChunk).toHaveBeenCalledWith(
expect.any(Object),
{ content: `\n\nError: ${rawValue}` },
'stop',
);
});
it('logs bounded metadata for tool callback failures', async () => {
const { logger } = require('@librechat/data-schemas');
const rawValue = 'PRIVATE-OPENAI-TOOL-PAYLOAD';
const toolError = Object.assign(new Error(`Tool echoed ${rawValue}`), {
code: 'ERR_TOOL',
response: { status: 422, data: { output: rawValue } },
});
mockProcessStream.mockImplementationOnce(async (_input, _config, options) => {
options.callbacks.TOOL_ERROR({}, toolError, 'execute_code');
});
await OpenAIChatCompletionController(req, res);
expect(mockGetSafeErrorMetadata).toHaveBeenCalledWith(toolError);
const errorLog = logger.error.mock.calls.find(([message]) =>
message.includes('Tool Error "execute_code"'),
);
expect(errorLog).toEqual([
'[OpenAI API] Tool Error "execute_code"',
{ type: 'Error', status: 422 },
]);
expect(JSON.stringify(errorLog)).not.toContain(rawValue);
});
});
describe('conversation ownership validation', () => {
it('should skip ownership check when conversation_id is not provided', async () => {
const { getConvo } = require('~/models');

View file

@ -14,6 +14,54 @@ const mockGetBalanceConfig = jest.fn().mockReturnValue({ enabled: true });
const mockGetTransactionsConfig = jest.fn().mockReturnValue({ enabled: true });
const mockResolveMemoryAvailability = jest.fn().mockResolvedValue(true);
const mockInitialSessions = new Map([['execute_code', { session_id: 'seeded' }]]);
const mockInspectContent = jest.fn().mockReturnValue(null);
const mockResolveConversationTitle = jest.fn(({ filters, candidate, fallback = 'New Chat' }) => {
const resolveAllowedTitle = (value) => {
if (typeof value !== 'string' || value.trim() === '') {
return null;
}
const finding = mockInspectContent(
[{ source: 'conversation_title', field: 'title', text: value }],
{ filters },
);
return finding == null ? value : null;
};
return (
resolveAllowedTitle(candidate) ??
(fallback === candidate ? null : resolveAllowedTitle(fallback))
);
});
const mockHasActivePiiPatterns = (config) =>
config != null &&
(config.starterPatterns == null ||
config.starterPatterns.length > 0 ||
(config.customPatterns?.length ?? 0) > 0);
const mockHasModelBoundContentProtection = (filters, legacyPii) => {
const sourcePolicies = [
legacyPii,
filters?.messages?.pii,
filters?.agentInstructions?.pii,
filters?.conversationStarters?.pii,
filters?.skills?.pii,
filters?.memories?.pii,
filters?.files?.pii,
filters?.toolArguments?.pii,
filters?.modelParameters?.pii,
filters?.actionMetadata?.pii,
];
if (sourcePolicies.some(mockHasActivePiiPatterns)) {
return true;
}
const filePolicy = filters?.files?.pii;
return (
filePolicy?.uninspectable === 'block' &&
(filePolicy.fields == null ||
filePolicy.fields.some((field) =>
['content', 'extracted_text', 'transcript'].includes(field),
))
);
};
class MockAgentRunEnvelopeError extends TypeError {
constructor(message) {
super(message);
@ -34,6 +82,13 @@ const mockCreateAgentRunEnvelope = jest.fn(
payload: JSON.parse(JSON.stringify(payload)),
}),
);
const mockGetSafeErrorMetadata = jest.fn((error) => {
const status = error?.status ?? error?.statusCode ?? error?.response?.status;
return {
type: error instanceof Error ? 'Error' : 'UnknownError',
...(Number.isInteger(status) && status >= 100 && status <= 599 && { status }),
};
});
const mockBuildSkillPrimedIdsByName = jest.fn((manualSkillPrimes, alwaysApplySkillPrimes) => {
const primed = {};
for (const skill of alwaysApplySkillPrimes ?? []) {
@ -110,10 +165,27 @@ jest.mock('@librechat/agents', () => ({
}));
jest.mock('@librechat/api', () => ({
SAFE_CONVERSATION_TITLE: 'New Chat',
resolveConversationTitle: (...args) => mockResolveConversationTitle(...args),
/** Pass-through: the controller strips UI-only activity-label parts
* before SDK formatting; the mock must expose it like any other used
* export or the call throws before the assertions run. */
stripActivityLabelParts: jest.fn((payload) => payload),
collectReachableAgents: (roots) => {
const agents = [];
const pending = [...roots];
const visited = new Set();
for (let index = 0; index < pending.length; index++) {
const agent = pending[index];
if (!agent || visited.has(agent)) {
continue;
}
visited.add(agent);
agents.push(agent);
pending.push(...(agent.subagentAgentConfigs ?? []));
}
return agents;
},
createRun: jest.fn().mockResolvedValue({
processStream: jest.fn().mockResolvedValue(undefined),
}),
@ -190,7 +262,35 @@ jest.mock('@librechat/api', () => ({
buildResponse: jest.fn().mockReturnValue({ id: 'resp_123', output: [] }),
generateResponseId: jest.fn().mockReturnValue('resp_mock-123'),
isValidationFailure: jest.fn().mockReturnValue(false),
findPiiMatchInMessages: jest.fn().mockReturnValue(null),
inspectContent: mockInspectContent,
extractConversationTitleContent: jest.fn(({ title }) => [
{ source: 'conversation_title', field: 'title', text: title },
]),
extractAgentContent: jest.fn().mockReturnValue([]),
extractFileContent: jest.fn().mockReturnValue([]),
extractMessageContent: jest.fn().mockReturnValue([]),
extractModelParameterContent: jest.fn().mockReturnValue([]),
extractSkillContent: jest.fn().mockReturnValue([]),
extractToolArgumentContent: jest.fn().mockReturnValue([]),
getBlockedOpaqueFileField: jest.fn().mockReturnValue(null),
getContentTraversalFragments: jest.fn().mockReturnValue([]),
isContentTraversalProtected: jest.fn().mockReturnValue(true),
isContentTraversalLimitError: jest.fn((error) => error?.code === 'content_filter_uninspectable'),
prependContentTraversalFragments: jest.fn(),
assertModelBoundContent: jest.fn(),
hasModelBoundContentProtection: mockHasModelBoundContentProtection,
isContentFilterError: jest.fn((error) => error?.code === 'content_filter_block'),
getSafeErrorMetadata: mockGetSafeErrorMetadata,
contentFilterBlockResponse: jest.fn().mockReturnValue({
error: 'content_filter_block',
message: 'Submitted content was blocked.',
}),
contentFilterUninspectableResponse: jest.fn().mockReturnValue({
error: 'content_filter_uninspectable',
message: 'Submitted file content could not be inspected before processing.',
source: 'file',
field: 'content',
}),
emitResponseCreated: jest.fn(),
createResponseContext: jest.fn().mockReturnValue({ responseId: 'resp_123' }),
createResponseTracker: jest.fn().mockReturnValue({
@ -341,6 +441,7 @@ describe('createResponse controller', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGlobalDiscoveredAgentConfigs = null;
require('@librechat/api').inspectContent.mockReset().mockReturnValue(null);
const controller = require('../responses');
createResponse = controller.createResponse;
@ -584,6 +685,813 @@ describe('createResponse controller', () => {
});
});
describe('content filtering', () => {
it('replaces a blocked agent-derived conversation title before persistence', async () => {
const api = require('@librechat/api');
const db = require('~/models');
api.validateResponseRequest.mockReturnValueOnce({
request: {
model: 'agent-123',
input: 'Hello',
stream: false,
store: true,
},
});
api.inspectContent.mockImplementation((fragments) =>
fragments[0]?.source === 'conversation_title' && fragments[0]?.text === 'BLOCKED-AGENT'
? { detectorId: 'pii-pattern' }
: null,
);
db.getAgent.mockResolvedValueOnce({
id: 'agent-123',
name: 'BLOCKED-AGENT',
model: 'claude-3',
provider: 'anthropic',
});
req.config.filters = {
conversationTitles: {
pii: {
starterPatterns: [],
customPatterns: [{ id: 'blocked', label: 'blocked', regex: 'BLOCKED' }],
},
},
};
await createResponse(req, res);
expect(db.saveConvo).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
title: 'New Chat',
}),
expect.anything(),
);
});
it('blocks opaque response input before conversion or agent loading', async () => {
const api = require('@librechat/api');
const db = require('~/models');
const input = [
{
type: 'message',
role: 'user',
content: [{ type: 'input_file', file_data: 'do-not-echo' }],
},
];
api.validateResponseRequest.mockReturnValueOnce({
request: { model: 'agent-123', input, stream: false },
});
api.getBlockedOpaqueFileField.mockReturnValueOnce('extracted_text');
api.contentFilterUninspectableResponse.mockReturnValueOnce({
error: 'content_filter_uninspectable',
message: 'Submitted file content could not be inspected before processing.',
source: 'file',
field: 'extracted_text',
});
await createResponse(req, res);
expect(api.getBlockedOpaqueFileField).toHaveBeenCalledWith(req.config.filters, input);
expect(api.convertInputToMessages).not.toHaveBeenCalled();
expect(db.getAgent).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
'Submitted file content could not be inspected before processing.',
'invalid_request',
'content_filter_uninspectable',
);
expect(JSON.stringify(api.sendResponsesErrorResponse.mock.calls)).not.toContain(
'do-not-echo',
);
});
it('returns a raw-free error when nested response input exhausts its budget', async () => {
const api = require('@librechat/api');
const db = require('~/models');
req.config.filters = { messages: { pii: { starterPatterns: [] } } };
api.extractMessageContent.mockImplementationOnce(() => {
throw {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted content could not be completely inspected before processing.',
source: 'message',
field: 'content_part',
},
};
});
await createResponse(req, res);
expect(db.getAgent).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
'Submitted content could not be completely inspected before processing.',
'invalid_request',
'content_filter_uninspectable',
);
});
it('continues when exhausted response parameters are outside the active policy', async () => {
const api = require('@librechat/api');
const db = require('~/models');
api.extractModelParameterContent.mockImplementationOnce(() => {
throw {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted content could not be completely inspected before processing.',
source: 'model_parameter',
field: 'request_fields',
},
};
});
api.isContentTraversalProtected.mockReturnValueOnce(false);
await createResponse(req, res);
expect(db.getAgent).toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).not.toHaveBeenCalledWith(
res,
400,
expect.anything(),
'invalid_request',
'content_filter_uninspectable',
);
});
it('retains earlier request fragments when a function schema exhausts traversal', async () => {
const api = require('@librechat/api');
const db = require('~/models');
const instructionFragment = {
id: 'agent.instructions',
path: '/instructions',
text: 'PRIVATE-INSTRUCTION',
source: 'agent_instruction',
field: 'instructions',
};
const partialToolFragment = {
id: 'tool.arguments.partial',
path: '/arguments/safe',
text: 'safe',
source: 'tool_argument',
field: 'arguments',
};
const traversalError = Object.assign(new Error('Traversal limit exceeded'), {
code: 'content_filter_uninspectable',
statusCode: 400,
body: {
error: 'content_filter_uninspectable',
message: 'Submitted content could not be completely inspected before processing.',
source: 'tool_argument',
field: 'arguments',
},
});
api.validateResponseRequest.mockReturnValueOnce({
request: {
model: 'agent-123',
input: 'Hello',
instructions: 'PRIVATE-INSTRUCTION',
tools: [{ type: 'function', name: 'lookup', parameters: { safe: true } }],
stream: false,
},
});
api.extractAgentContent.mockReturnValueOnce([instructionFragment]);
api.extractToolArgumentContent.mockImplementationOnce(() => {
throw traversalError;
});
api.getContentTraversalFragments.mockReturnValueOnce([
instructionFragment,
partialToolFragment,
]);
api.inspectContent.mockReturnValueOnce({
detectorId: 'pii-pattern',
ruleId: 'private',
label: 'private value',
source: 'agent_instruction',
field: 'instructions',
});
req.config.filters = {
agentInstructions: {
pii: {
fields: ['instructions'],
starterPatterns: [],
customPatterns: [{ id: 'private', label: 'private value', regex: 'PRIVATE' }],
},
},
};
await createResponse(req, res);
expect(api.prependContentTraversalFragments).toHaveBeenCalledWith(
traversalError,
expect.arrayContaining([instructionFragment]),
);
expect(api.inspectContent).toHaveBeenCalledWith(
expect.arrayContaining([instructionFragment, partialToolFragment]),
expect.anything(),
);
expect(db.getAgent).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
'Submitted content was blocked.',
'invalid_request',
'content_filter_block',
);
});
it('blocks instructions and input before loading the agent', async () => {
const api = require('@librechat/api');
const db = require('~/models');
api.validateResponseRequest.mockReturnValueOnce({
request: {
model: 'agent-123',
input: 'Hello',
stream: false,
metadata: { label: 'submitted metadata' },
text: {
format: {
type: 'json_schema',
json_schema: { description: 'submitted response schema' },
},
},
},
});
api.inspectContent.mockReturnValueOnce({
detectorId: 'pii-pattern',
ruleId: 'sk_prefix',
label: 'sk- prefix token',
source: 'agent_instruction',
field: 'instructions',
});
await createResponse(req, res);
expect(api.extractAgentContent).toHaveBeenCalled();
expect(api.extractMessageContent).toHaveBeenCalled();
expect(api.extractModelParameterContent).toHaveBeenCalledWith(
expect.objectContaining({
metadata: { label: 'submitted metadata' },
response_format: {
type: 'json_schema',
json_schema: { description: 'submitted response schema' },
},
}),
);
expect(db.getAgent).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
'Submitted content was blocked.',
'invalid_request',
'content_filter_block',
);
});
it('blocks manually selected skill names before resolving the skill', async () => {
const api = require('@librechat/api');
const db = require('~/models');
req.body.manualSkills = ['PRIVATE-SKILL'];
api.extractManualSkills.mockReturnValueOnce(['PRIVATE-SKILL']);
api.inspectContent.mockReturnValueOnce({
detectorId: 'pii-pattern',
ruleId: 'private',
label: 'private value',
source: 'skill',
field: 'name',
});
await createResponse(req, res);
expect(api.extractSkillContent).toHaveBeenCalledWith({ name: 'PRIVATE-SKILL' });
expect(db.getAgent).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
'Submitted content was blocked.',
'invalid_request',
'content_filter_block',
);
});
it('blocks previously stored model-bound content before provider invocation', async () => {
const api = require('@librechat/api');
const blockedError = Object.assign(
new Error('Submitted content contains a private value. Remove it and try again.'),
{
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content contains a private value. Remove it and try again.',
source: 'message',
field: 'text',
},
},
);
api.assertModelBoundContent.mockImplementationOnce(() => {
throw blockedError;
});
await createResponse(req, res);
expect(api.assertModelBoundContent).toHaveBeenCalled();
expect(api.createRun).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
blockedError.body.message,
'invalid_request',
'content_filter_block',
);
});
it('preserves imported whole-assistant provenance and blocks its stored text', async () => {
const api = require('@librechat/api');
const db = require('~/models');
const storedMessage = {
messageId: 'imported-assistant',
isCreatedByUser: false,
isUserSubmitted: true,
text: 'sk-imported-secret',
};
const blockedError = Object.assign(new Error('Submitted content was blocked.'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content was blocked.',
source: 'message',
field: 'text',
},
});
api.validateResponseRequest.mockReturnValueOnce({
request: {
model: 'agent-123',
input: 'Hello',
stream: false,
previous_response_id: 'resp_imported',
},
});
db.getConvo.mockResolvedValueOnce({ conversationId: 'resp_imported', user: 'user-123' });
db.getMessages.mockResolvedValueOnce([storedMessage]);
api.assertModelBoundContent.mockImplementationOnce(({ storedMessages }) => {
expect(storedMessages).toEqual([
expect.objectContaining({
messageId: 'imported-assistant',
isCreatedByUser: false,
isUserSubmitted: true,
text: 'sk-imported-secret',
content: 'sk-imported-secret',
}),
]);
throw blockedError;
});
await createResponse(req, res);
expect(api.initializeAgent).not.toHaveBeenCalled();
expect(api.discoverConnectedAgents).not.toHaveBeenCalled();
expect(mockBuildAgentScopedContext).not.toHaveBeenCalled();
expect(mockApplyContextToAgent).not.toHaveBeenCalled();
expect(db.updateFilesUsage).not.toHaveBeenCalled();
expect(api.createRun).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
blockedError.body.message,
'invalid_request',
'content_filter_block',
);
});
it('preserves path-marked assistant content and blocks only the submitted block', async () => {
const api = require('@librechat/api');
const db = require('~/models');
const content = [
{ type: 'text', text: 'neighboring model prose' },
{ type: 'text', text: 'sk-path-secret' },
];
const blockedError = Object.assign(new Error('Submitted content was blocked.'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content was blocked.',
source: 'message',
field: 'content_part',
},
});
api.validateResponseRequest.mockReturnValueOnce({
request: {
model: 'agent-123',
input: 'Hello',
stream: false,
previous_response_id: 'resp_path_marked',
},
});
db.getConvo.mockResolvedValueOnce({
conversationId: 'resp_path_marked',
user: 'user-123',
});
db.getMessages.mockResolvedValueOnce([
{
messageId: 'mixed-assistant',
isCreatedByUser: false,
text: 'assistant summary text',
content,
userSubmittedPaths: ['/content/1/text'],
userSubmittedMessageFieldPaths: [{ path: '/content/1/text', field: 'decision_response' }],
},
]);
api.assertModelBoundContent.mockImplementationOnce(({ storedMessages }) => {
expect(storedMessages).toEqual([
expect.objectContaining({
text: 'assistant summary text',
content,
userSubmittedPaths: ['/content/1/text'],
userSubmittedMessageFieldPaths: [
{ path: '/content/1/text', field: 'decision_response' },
],
}),
]);
throw blockedError;
});
await createResponse(req, res);
expect(api.initializeAgent).not.toHaveBeenCalled();
expect(api.createRun).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
blockedError.body.message,
'invalid_request',
'content_filter_block',
);
});
it('allows neighboring unmarked assistant prose when marked content is safe', async () => {
const api = require('@librechat/api');
const db = require('~/models');
const content = [
{ type: 'text', text: 'sk-model-generated-prose' },
{ type: 'text', text: 'safe submitted correction' },
];
api.validateResponseRequest.mockReturnValueOnce({
request: {
model: 'agent-123',
input: 'Hello',
stream: false,
previous_response_id: 'resp_neighboring_model_output',
},
});
db.getConvo.mockResolvedValueOnce({
conversationId: 'resp_neighboring_model_output',
user: 'user-123',
});
db.getMessages.mockResolvedValueOnce([
{
messageId: 'mixed-assistant',
isCreatedByUser: false,
content,
userSubmittedPaths: ['/content/1/text'],
},
]);
api.assertModelBoundContent.mockImplementationOnce(({ storedMessages }) => {
const [message] = storedMessages;
expect(message.content).toEqual(content);
expect(message.userSubmittedPaths).toEqual(['/content/1/text']);
expect(message.content[1].text).toBe('safe submitted correction');
});
await createResponse(req, res);
expect(api.createRun).toHaveBeenCalledTimes(1);
expect(api.sendResponsesErrorResponse).not.toHaveBeenCalledWith(
res,
400,
expect.anything(),
'invalid_request',
'content_filter_block',
);
});
it('preflights request and context attachments from every run agent under a files-only policy', async () => {
const api = require('@librechat/api');
const primaryRequestFile = { filename: 'primary-request.txt', content: 'primary request' };
const primaryContextFile = { filename: 'primary-context.txt', content: 'primary context' };
const handoffRequestFile = { filename: 'handoff-request.txt', content: 'handoff request' };
const handoffContextFile = {
filename: 'handoff-context.txt',
content: 'sk-handoff-context',
};
const blockedError = Object.assign(new Error('Submitted file content was blocked.'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted file content was blocked.',
source: 'file',
field: 'content',
},
});
req.config.filters = {
files: { pii: { fields: ['content'], starterPatterns: ['sk-'] } },
};
api.validateResponseRequest.mockReturnValueOnce({
request: { model: 'agent-123', input: 'Hello', stream: true },
});
api.initializeAgent.mockResolvedValueOnce({
id: 'agent-123',
model: 'claude-3',
model_parameters: {},
toolRegistry: {},
edges: [{ source: 'agent-123', target: 'agent-handoff' }],
requestAttachments: [primaryRequestFile],
agentContextAttachments: [primaryContextFile],
});
mockGlobalDiscoveredAgentConfigs = new Map([
[
'agent-handoff',
{
id: 'agent-handoff',
model: 'claude-3',
model_parameters: {},
requestAttachments: [handoffRequestFile],
agentContextAttachments: [handoffContextFile],
},
],
]);
api.assertModelBoundContent
.mockImplementationOnce(() => undefined)
.mockImplementationOnce(({ filters, files }) => {
expect(filters).toEqual(req.config.filters);
expect(files).toEqual([
primaryRequestFile,
primaryContextFile,
handoffRequestFile,
handoffContextFile,
]);
throw blockedError;
});
await createResponse(req, res);
expect(api.createRun).not.toHaveBeenCalled();
expect(api.setupStreamingResponse).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
blockedError.body.message,
'invalid_request',
'content_filter_block',
);
});
it('preflights each exact synthesized dynamic tool context as file content', async () => {
const api = require('@librechat/api');
const nestedPureSubagent = {
id: 'agent-nested-pure',
model: 'claude-3',
model_parameters: {},
toolDefinitions: [
{
name: 'nested_lookup',
description: 'late-loaded nested tool definition',
parameters: { type: 'object' },
},
],
dynamicToolContextMap: {
nested_lookup: 'sk-nested-dynamic-context',
},
};
const blockedError = Object.assign(new Error('Submitted file content was blocked.'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted file content was blocked.',
source: 'file',
field: 'content',
},
});
req.config.filters = {
files: { pii: { fields: ['content'], starterPatterns: ['sk-'] } },
};
api.validateResponseRequest.mockReturnValueOnce({
request: { model: 'agent-123', input: 'Hello', stream: true },
});
api.initializeAgent.mockResolvedValueOnce({
id: 'agent-123',
model: 'claude-3',
model_parameters: {},
toolRegistry: {},
edges: [{ source: 'agent-123', target: 'agent-handoff' }],
dynamicToolContextMap: {
execute_code: ' primary safe context',
ignored_empty: '',
file_search: 'primary context ',
ignored_non_string: 42,
},
subagentAgentConfigs: [
{
id: 'agent-pure',
model: 'claude-3',
model_parameters: {},
subagentAgentConfigs: [nestedPureSubagent],
},
],
});
mockGlobalDiscoveredAgentConfigs = new Map([
[
'agent-handoff',
{
id: 'agent-handoff',
model: 'claude-3',
model_parameters: {},
dynamicToolContextMap: {
execute_code: 'handoff safe context',
file_search: 'sk-handoff-dynamic-context',
},
},
],
]);
api.assertModelBoundContent
.mockImplementationOnce(() => undefined)
.mockImplementationOnce(({ filters, agents, files }) => {
expect(filters).toEqual(req.config.filters);
expect(agents.map(({ id }) => id)).toEqual([
'agent-123',
'agent-handoff',
'agent-pure',
'agent-nested-pure',
]);
expect(files).toEqual([
{ content: 'primary safe context\nprimary context' },
{ content: 'handoff safe context\nsk-handoff-dynamic-context' },
{ content: 'sk-nested-dynamic-context' },
]);
throw blockedError;
});
await createResponse(req, res);
expect(api.createRun).not.toHaveBeenCalled();
expect(api.setupStreamingResponse).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
blockedError.body.message,
'invalid_request',
'content_filter_block',
);
});
it('re-inspects agents after dynamic context is applied', async () => {
const api = require('@librechat/api');
const blockedError = Object.assign(new Error('Submitted content was blocked.'), {
code: 'content_filter_block',
statusCode: 400,
body: {
error: 'content_filter_block',
message: 'Submitted content was blocked.',
source: 'agent_instruction',
field: 'instructions',
},
});
mockApplyContextToAgent.mockImplementationOnce(async ({ agent }) => {
agent.instructions = 'PRIVATE-DYNAMIC-INSTRUCTION';
});
api.assertModelBoundContent
.mockImplementationOnce(() => undefined)
.mockImplementationOnce(({ agents }) => {
if (agents?.some((agent) => agent.instructions === 'PRIVATE-DYNAMIC-INSTRUCTION')) {
throw blockedError;
}
});
await createResponse(req, res);
expect(api.assertModelBoundContent).toHaveBeenLastCalledWith(
expect.objectContaining({
agents: [expect.objectContaining({ instructions: 'PRIVATE-DYNAMIC-INSTRUCTION' })],
}),
);
expect(api.createRun).not.toHaveBeenCalled();
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
400,
blockedError.body.message,
'invalid_request',
'content_filter_block',
);
});
});
describe('safe error logging', () => {
it('logs bounded metadata and returns a raw-free provider error', async () => {
const api = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const rawValue = 'PRIVATE-RESPONSES-PROVIDER-PAYLOAD';
const providerError = Object.assign(new Error(`Provider echoed ${rawValue}`), {
code: 'ERR_REMOTE',
response: {
status: 502,
headers: { authorization: rawValue },
data: { prompt: rawValue },
},
});
req.config.filters = { messages: { pii: {} } };
api.createRun.mockRejectedValueOnce(providerError);
await createResponse(req, res);
expect(mockGetSafeErrorMetadata).toHaveBeenCalledWith(providerError);
const errorLog = logger.error.mock.calls.find(
([message]) => message === '[Responses API] Error:',
);
expect(errorLog).toEqual(['[Responses API] Error:', { type: 'Error', status: 502 }]);
expect(JSON.stringify(errorLog)).not.toContain(rawValue);
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
500,
'An error occurred while processing the request',
'server_error',
);
expect(JSON.stringify(api.sendResponsesErrorResponse.mock.calls)).not.toContain(rawValue);
});
it('preserves the legacy provider error when protection is inactive', async () => {
const api = require('@librechat/api');
const rawValue = 'LEGACY-RESPONSES-PROVIDER-ERROR';
api.createRun.mockRejectedValueOnce(
Object.assign(new Error(rawValue), { code: 'ERR_LEGACY_REMOTE' }),
);
await createResponse(req, res);
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
500,
rawValue,
'server_error',
'ERR_LEGACY_REMOTE',
);
});
it.each([
['a management-only prompt', { prompts: { pii: {} } }],
['an inert message', { messages: { pii: { starterPatterns: [] } } }],
])('preserves the legacy provider error for %s policy', async (_policy, filters) => {
const api = require('@librechat/api');
const rawValue = 'LEGACY-RESPONSES-CONFIGURED-PROVIDER-ERROR';
req.config.filters = filters;
api.createRun.mockRejectedValueOnce(new Error(rawValue));
await createResponse(req, res);
expect(api.sendResponsesErrorResponse).toHaveBeenCalledWith(
res,
500,
rawValue,
'server_error',
);
});
it('logs bounded metadata for tool callback failures', async () => {
const api = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const rawValue = 'PRIVATE-RESPONSES-TOOL-PAYLOAD';
const toolError = Object.assign(new Error(`Tool echoed ${rawValue}`), {
code: 'ERR_TOOL',
response: { status: 422, data: { output: rawValue } },
});
api.createRun.mockResolvedValueOnce({
processStream: jest.fn().mockImplementation(async (_input, _config, options) => {
options.callbacks.TOOL_ERROR({}, toolError, 'file_search');
}),
});
await createResponse(req, res);
expect(mockGetSafeErrorMetadata).toHaveBeenCalledWith(toolError);
const errorLog = logger.error.mock.calls.find(([message]) =>
message.includes('Tool Error "file_search"'),
);
expect(errorLog).toEqual([
'[Responses API] Tool Error "file_search"',
{ type: 'Error', status: 422 },
]);
expect(JSON.stringify(errorLog)).not.toContain(rawValue);
});
});
describe('conversation ownership validation', () => {
it('should skip ownership check when previous_response_id is not provided', async () => {
const { getConvo } = require('~/models');

File diff suppressed because it is too large Load diff