🪢 refactor: Move Agent Execution Seam Before Initialization (#14581)

* refactor: move agent execution seam before initialization

* refactor: type librechat agent request extensions

* refactor: read envelope values from descriptors

* fix: preserve envelope types and validation errors

* fix: bound agent envelope traversal
This commit is contained in:
Danny Avila 2026-08-01 18:25:34 -04:00 committed by GitHub
parent c2d8252b4f
commit 59395a6bf0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 683 additions and 59 deletions

View file

@ -11,6 +11,26 @@ const mockRecordCollectedUsage = jest
.mockResolvedValue({ input_tokens: 100, output_tokens: 50 });
const mockGetBalanceConfig = jest.fn().mockReturnValue({ enabled: true });
const mockGetTransactionsConfig = jest.fn().mockReturnValue({ enabled: true });
class MockAgentRunEnvelopeError extends TypeError {
constructor(message) {
super(message);
this.name = 'AgentRunEnvelopeError';
}
}
const mockCreateAgentRunEnvelope = jest.fn(
({ protocol, requestId, receivedAt, principal, payload }) => ({
version: 1,
protocol,
requestId,
receivedAt,
principal: {
userId: principal.id,
...(principal.role != null && { role: principal.role }),
...(principal.tenantId != null && { tenantId: principal.tenantId }),
},
payload: JSON.parse(JSON.stringify(payload)),
}),
);
const mockBuildSkillPrimedIdsByName = jest.fn((manualSkillPrimes, alwaysApplySkillPrimes) => {
const primed = {};
for (const skill of alwaysApplySkillPrimes ?? []) {
@ -88,6 +108,8 @@ jest.mock('@librechat/api', () => ({
}),
createChunk: jest.fn().mockReturnValue({}),
buildToolSet: jest.fn().mockReturnValue(new Set()),
AgentRunEnvelopeError: MockAgentRunEnvelopeError,
createAgentRunEnvelope: (...args) => mockCreateAgentRunEnvelope(...args),
scopeSkillIds: jest.fn().mockImplementation((ids) => ids),
resolveAgentScopedSkillIds: jest
.fn()
@ -331,6 +353,60 @@ describe('OpenAIChatCompletionController', () => {
});
});
describe('execution envelope', () => {
it('creates the portable run input before agent initialization', async () => {
req.user = {
id: 'user-123',
role: 'USER',
tenantId: 'tenant-123',
federatedTokens: { access_token: 'secret' },
};
const requestBody = {
...req.body,
ephemeralAgent: { skills: true },
manualSkills: ['review-code'],
timezone: 'America/New_York',
};
req.body = requestBody;
const { validateRequest, initializeAgent } = require('@librechat/api');
validateRequest.mockReturnValueOnce({ request: requestBody });
await OpenAIChatCompletionController(req, res);
expect(mockCreateAgentRunEnvelope).toHaveBeenCalledWith(
expect.objectContaining({
protocol: 'chat.completions',
principal: req.user,
payload: requestBody,
requestId: expect.any(String),
receivedAt: expect.any(Number),
}),
);
expect(mockCreateAgentRunEnvelope.mock.invocationCallOrder[0]).toBeLessThan(
initializeAgent.mock.invocationCallOrder[0],
);
expect(req.body).not.toBe(requestBody);
expect(req.body).toEqual(requestBody);
expect(JSON.stringify(mockCreateAgentRunEnvelope.mock.results[0].value)).not.toContain(
'secret',
);
});
it('returns a protocol 400 when the envelope rejects a non-JSON payload', async () => {
const message = 'payload.max_tokens must contain only finite numbers';
const { createErrorResponse, initializeAgent } = require('@librechat/api');
mockCreateAgentRunEnvelope.mockImplementationOnce(() => {
throw new MockAgentRunEnvelopeError(message);
});
await OpenAIChatCompletionController(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(createErrorResponse).toHaveBeenCalledWith(message, 'invalid_request_error', null);
expect(initializeAgent).not.toHaveBeenCalled();
});
});
describe('token usage recording', () => {
it('should call recordCollectedUsage after successful non-streaming completion', async () => {
await OpenAIChatCompletionController(req, res);

View file

@ -10,6 +10,26 @@ const mockRecordCollectedUsage = jest
.mockResolvedValue({ input_tokens: 100, output_tokens: 50 });
const mockGetBalanceConfig = jest.fn().mockReturnValue({ enabled: true });
const mockGetTransactionsConfig = jest.fn().mockReturnValue({ enabled: true });
class MockAgentRunEnvelopeError extends TypeError {
constructor(message) {
super(message);
this.name = 'AgentRunEnvelopeError';
}
}
const mockCreateAgentRunEnvelope = jest.fn(
({ protocol, requestId, receivedAt, principal, payload }) => ({
version: 1,
protocol,
requestId,
receivedAt,
principal: {
userId: principal.id,
...(principal.role != null && { role: principal.role }),
...(principal.tenantId != null && { tenantId: principal.tenantId }),
},
payload: JSON.parse(JSON.stringify(payload)),
}),
);
const mockBuildSkillPrimedIdsByName = jest.fn((manualSkillPrimes, alwaysApplySkillPrimes) => {
const primed = {};
for (const skill of alwaysApplySkillPrimes ?? []) {
@ -93,6 +113,8 @@ jest.mock('@librechat/api', () => ({
}),
applyContextToAgent: (...args) => mockApplyContextToAgent(...args),
buildToolSet: jest.fn().mockReturnValue(new Set()),
AgentRunEnvelopeError: MockAgentRunEnvelopeError,
createAgentRunEnvelope: (...args) => mockCreateAgentRunEnvelope(...args),
buildAgentScopedContext: (...args) => mockBuildAgentScopedContext(...args),
buildAgentContextAttachmentsByAgentId: (...args) =>
mockBuildAgentContextAttachmentsByAgentId(...args),
@ -319,6 +341,60 @@ describe('createResponse controller', () => {
};
});
describe('execution envelope', () => {
it('creates the portable run input before agent initialization', async () => {
req.user = {
id: 'user-123',
role: 'USER',
tenantId: 'tenant-123',
federatedTokens: { access_token: 'secret' },
};
const requestBody = {
...req.body,
ephemeralAgent: { skills: true },
manualSkills: ['review-code'],
timezone: 'America/New_York',
isTemporary: true,
};
req.body = requestBody;
const { validateResponseRequest, initializeAgent } = require('@librechat/api');
validateResponseRequest.mockReturnValueOnce({ request: requestBody });
await createResponse(req, res);
expect(mockCreateAgentRunEnvelope).toHaveBeenCalledWith(
expect.objectContaining({
protocol: 'responses',
principal: req.user,
payload: requestBody,
requestId: expect.any(String),
receivedAt: expect.any(Number),
}),
);
expect(mockCreateAgentRunEnvelope.mock.invocationCallOrder[0]).toBeLessThan(
initializeAgent.mock.invocationCallOrder[0],
);
expect(req.body).not.toBe(requestBody);
expect(req.body).toEqual(requestBody);
expect(JSON.stringify(mockCreateAgentRunEnvelope.mock.results[0].value)).not.toContain(
'secret',
);
});
it('returns a protocol 400 when the envelope rejects a non-JSON payload', async () => {
const message = 'payload.max_output_tokens must contain only finite numbers';
const { sendResponsesErrorResponse, initializeAgent } = require('@librechat/api');
mockCreateAgentRunEnvelope.mockImplementationOnce(() => {
throw new MockAgentRunEnvelopeError(message);
});
await createResponse(req, res);
expect(sendResponsesErrorResponse).toHaveBeenCalledWith(res, 400, message, 'invalid_request');
expect(initializeAgent).not.toHaveBeenCalled();
});
});
describe('conversation ownership validation', () => {
it('should skip ownership check when previous_response_id is not provided', async () => {
const { getConvo } = require('~/models');

View file

@ -13,6 +13,8 @@ const {
createRun,
createChunk,
buildToolSet,
AgentRunEnvelopeError,
createAgentRunEnvelope,
loadSkillStates,
sendFinalChunk,
createSafeUser,
@ -149,29 +151,20 @@ function sendErrorResponse(res, statusCode, message, type = 'invalid_request_err
}
/**
* OpenAI-compatible chat completions controller for agents.
* Runs a validated chat-completions envelope in the current process.
* Express remains runtime-only state while the envelope is the portable run input.
*
* POST /v1/chat/completions
*
* Request format:
* {
* "model": "agent_id_here",
* "messages": [{"role": "user", "content": "Hello!"}],
* "stream": true,
* "conversation_id": "optional",
* "parent_message_id": "optional"
* }
* @param {import('@librechat/api').ChatCompletionRunEnvelope} envelope
* @param {{req: import('express').Request, res: import('express').Response}} runtime
*/
const OpenAIChatCompletionController = async (req, res) => {
const executeOpenAIChatCompletion = async (envelope, { req, res }) => {
const appConfig = req.config;
const requestStartTime = Date.now();
const validation = validateRequest(req.body);
if (isChatCompletionValidationFailure(validation)) {
return sendErrorResponse(res, 400, validation.error);
}
const request = validation.request;
const requestStartTime = envelope.receivedAt;
const request = envelope.payload;
const { principal } = envelope;
// The local executor keeps the current Express-dependent initialization path,
// but all request-body reads now observe the detached envelope payload.
req.body = request;
const agentId = request.model;
// Look up the agent
@ -232,7 +225,7 @@ const OpenAIChatCompletionController = async (req, res) => {
'invalid_request_error',
);
}
if (!(await db.getConvo(req.user?.id, request.conversation_id))) {
if (!(await db.getConvo(principal.userId, request.conversation_id))) {
return sendErrorResponse(res, 404, 'Conversation not found', 'invalid_request_error');
}
}
@ -277,12 +270,12 @@ const OpenAIChatCompletionController = async (req, res) => {
const enabledCapabilities = new Set(agentsEConfig?.capabilities);
const skillsCapabilityEnabled = enabledCapabilities.has(AgentCapabilities.skills);
const ephemeralSkillsToggle = req.body?.ephemeralAgent?.skills === true;
const ephemeralSkillsToggle = request.ephemeralAgent?.skills === true;
const accessibleSkillIds = skillsCapabilityEnabled
? withDeploymentSkillIds(
await findAccessibleResources({
userId: req.user.id,
role: req.user.role,
userId: principal.userId,
role: principal.role,
resourceType: ResourceType.SKILL,
requiredPermissions: PermissionBits.VIEW,
}),
@ -290,8 +283,8 @@ const OpenAIChatCompletionController = async (req, res) => {
: [];
const editableSkillIds = skillsCapabilityEnabled
? await findAccessibleResources({
userId: req.user.id,
role: req.user.role,
userId: principal.userId,
role: principal.role,
resourceType: ResourceType.SKILL,
requiredPermissions: PermissionBits.EDIT,
})
@ -301,13 +294,13 @@ const OpenAIChatCompletionController = async (req, res) => {
: false;
const { skillStates, defaultActiveOnShare } = await loadSkillStates({
userId: req.user.id,
userId: principal.userId,
appConfig,
getUserById: db.getUserById,
accessibleSkillIds,
});
const manualSkills = extractManualSkills(req.body);
const manualSkills = extractManualSkills(request);
const primaryScopedSkillIds = resolveAgentScopedSkillIds({
agent,
@ -741,7 +734,7 @@ const OpenAIChatCompletionController = async (req, res) => {
};
// Create and run the agent
const userId = req.user?.id ?? 'api-user';
const userId = principal.userId;
// Extract merged userMCPAuthMap (needed for MCP tool connections across
// the primary and any discovered handoff sub-agents)
@ -764,7 +757,7 @@ const OpenAIChatCompletionController = async (req, res) => {
conversationId,
},
user: { id: userId },
tenantId: req.user?.tenantId,
tenantId: principal.tenantId,
/** Bills subagent child-run model calls (reported outside the
* streamEvents loop) into the same collectedUsage array. */
subagentUsageSink: createSubagentUsageSink(collectedUsage),
@ -896,6 +889,38 @@ const OpenAIChatCompletionController = async (req, res) => {
}
};
/**
* OpenAI-compatible chat completions ingress adapter for agents.
* Authentication and remote-agent authorization have already run in route middleware.
*
* POST /v1/chat/completions
*/
const OpenAIChatCompletionController = async (req, res) => {
const receivedAt = Date.now();
const validation = validateRequest(req.body);
if (isChatCompletionValidationFailure(validation)) {
return sendErrorResponse(res, 400, validation.error);
}
let envelope;
try {
envelope = createAgentRunEnvelope({
protocol: 'chat.completions',
requestId: req.requestId ?? req.id ?? `agent-run-${nanoid()}`,
receivedAt,
principal: req.user,
payload: validation.request,
});
} catch (error) {
if (error instanceof AgentRunEnvelopeError) {
return sendErrorResponse(res, 400, error.message, 'invalid_request_error');
}
throw error;
}
return executeOpenAIChatCompletion(envelope, { req, res });
};
/**
* List available agents as models (filtered by remote access permissions)
*

View file

@ -13,6 +13,8 @@ const {
createRun,
applyContextToAgent,
buildToolSet,
AgentRunEnvelopeError,
createAgentRunEnvelope,
buildAgentScopedContext,
buildAgentContextAttachmentsByAgentId,
createSafeUser,
@ -284,25 +286,20 @@ function convertMessagesToOutputItems(messages) {
}
/**
* Create Response - POST /v1/responses
* Runs a validated Responses envelope in the current process.
* Express remains runtime-only state while the envelope is the portable run input.
*
* Creates a model response following the Open Responses API specification.
* Supports both streaming and non-streaming responses.
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('@librechat/api').ResponsesRunEnvelope} envelope
* @param {{req: import('express').Request, res: import('express').Response}} runtime
*/
const createResponse = async (req, res) => {
const executeResponse = async (envelope, { req, res }) => {
const appConfig = req.config;
const requestStartTime = Date.now();
// Validate request
const validation = validateResponseRequest(req.body);
if (isValidationFailure(validation)) {
return sendResponsesErrorResponse(res, 400, validation.error);
}
const request = validation.request;
const requestStartTime = envelope.receivedAt;
const request = envelope.payload;
const { principal } = envelope;
// The local executor keeps the current Express-dependent initialization path,
// but all request-body reads now observe the detached envelope payload.
req.body = request;
const agentId = request.model;
const isStreaming = request.stream === true;
const summarizationConfig = appConfig?.summarization;
@ -348,7 +345,7 @@ const createResponse = async (req, res) => {
'invalid_request',
);
}
if (!(await db.getConvo(req.user?.id, request.previous_response_id))) {
if (!(await db.getConvo(principal.userId, request.previous_response_id))) {
return sendResponsesErrorResponse(res, 404, 'Conversation not found', 'not_found');
}
}
@ -397,12 +394,12 @@ const createResponse = async (req, res) => {
appConfig?.endpoints?.[EModelEndpoint.agents]?.capabilities,
);
const skillsCapabilityEnabled = enabledCapabilities.has(AgentCapabilities.skills);
const ephemeralSkillsToggle = req.body?.ephemeralAgent?.skills === true;
const ephemeralSkillsToggle = request.ephemeralAgent?.skills === true;
const accessibleSkillIds = skillsCapabilityEnabled
? withDeploymentSkillIds(
await findAccessibleResources({
userId: req.user.id,
role: req.user.role,
userId: principal.userId,
role: principal.role,
resourceType: ResourceType.SKILL,
requiredPermissions: PermissionBits.VIEW,
}),
@ -410,8 +407,8 @@ const createResponse = async (req, res) => {
: [];
const editableSkillIds = skillsCapabilityEnabled
? await findAccessibleResources({
userId: req.user.id,
role: req.user.role,
userId: principal.userId,
role: principal.role,
resourceType: ResourceType.SKILL,
requiredPermissions: PermissionBits.EDIT,
})
@ -421,13 +418,13 @@ const createResponse = async (req, res) => {
: false;
const { skillStates, defaultActiveOnShare } = await loadSkillStates({
userId: req.user.id,
userId: principal.userId,
appConfig,
getUserById: db.getUserById,
accessibleSkillIds,
});
const manualSkills = extractManualSkills(req.body);
const manualSkills = extractManualSkills(request);
const primaryScopedSkillIds = resolveAgentScopedSkillIds({
agent,
@ -611,7 +608,7 @@ const createResponse = async (req, res) => {
// Load previous messages if previous_response_id is provided
let previousMessages = [];
if (request.previous_response_id) {
const userId = req.user?.id ?? 'api-user';
const userId = principal.userId;
previousMessages = await loadPreviousMessages(request.previous_response_id, userId);
}
@ -777,7 +774,7 @@ const createResponse = async (req, res) => {
};
// Create and run the agent
const userId = req.user?.id ?? 'api-user';
const userId = principal.userId;
const userMCPAuthMap = mergedMCPAuthMap;
const run = await createRun({
@ -795,7 +792,7 @@ const createResponse = async (req, res) => {
conversationId,
},
user: { id: userId },
tenantId: req.user?.tenantId,
tenantId: principal.tenantId,
/** Bills subagent child-run model calls (reported outside the
* streamEvents loop) into the same collectedUsage array. */
subagentUsageSink: createSubagentUsageSink(collectedUsage),
@ -958,7 +955,7 @@ const createResponse = async (req, res) => {
: {}),
};
const userId = req.user?.id ?? 'api-user';
const userId = principal.userId;
const userMCPAuthMap = mergedMCPAuthMap;
const run = await createRun({
@ -976,7 +973,7 @@ const createResponse = async (req, res) => {
conversationId,
},
user: { id: userId },
tenantId: req.user?.tenantId,
tenantId: principal.tenantId,
/** Bills subagent child-run model calls (reported outside the
* streamEvents loop) into the same collectedUsage array. */
subagentUsageSink: createSubagentUsageSink(collectedUsage),
@ -1090,6 +1087,41 @@ const createResponse = async (req, res) => {
}
};
/**
* Open Responses ingress adapter for agents.
* Authentication and remote-agent authorization have already run in route middleware.
*
* POST /v1/responses
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
const createResponse = async (req, res) => {
const receivedAt = Date.now();
const validation = validateResponseRequest(req.body);
if (isValidationFailure(validation)) {
return sendResponsesErrorResponse(res, 400, validation.error);
}
let envelope;
try {
envelope = createAgentRunEnvelope({
protocol: 'responses',
requestId: req.requestId ?? req.id ?? `agent-run-${nanoid()}`,
receivedAt,
principal: req.user,
payload: validation.request,
});
} catch (error) {
if (error instanceof AgentRunEnvelopeError) {
return sendResponsesErrorResponse(res, 400, error.message, 'invalid_request');
}
throw error;
}
return executeResponse(envelope, { req, res });
};
/**
* List available agents as models - GET /v1/models (also works with /v1/responses/models)
*