Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8a4fad7
⏲️ feat: Defer Loading MCP Tools (#11270)
danny-avila Jan 9, 2026
ebf5fb2
πŸ¦₯ feat: Add Deferred Tools as Agents Capability (#11295)
danny-avila Jan 11, 2026
be45420
πŸ” feat: Admin Auth. Routes with Secure Cross-Origin Token Exchange (#…
danny-avila Jan 11, 2026
93ef35f
🌏 fix: Update UI text for `com_ui_analyzing`
danny-avila Jan 11, 2026
fcaec85
πŸ—‚οΈ feat: Better Persistence for Code Execution Files Between Sessions…
danny-avila Jan 16, 2026
53067b6
πŸ‘€ feat: AWS Bedrock Custom Inference Profiles (#11308)
dustinhealy Jan 16, 2026
923fde5
πŸ“¦ chore: Bump `@librechat/agents` to v3.1.0
danny-avila Jan 19, 2026
7e57b83
πŸͺͺ feat: Microsoft Graph Access Token Placeholder for MCP Servers (#10…
maxesse Jan 19, 2026
842ac95
πŸ›Έ feat: Remote Agent Access with External API Support (#11503)
danny-avila Jan 26, 2026
d333a2c
fix: duplicate imports
danny-avila Jan 1, 2026
269d66f
Fix mock implementation for isEnabled function
danny-avila Jan 1, 2026
659fd43
Update mock for isEnabled function in tests
danny-avila Jan 1, 2026
d7c107e
Refactor AuthService test mocks
danny-avila Jan 1, 2026
6243bfe
Add default token expiry constants in tests
danny-avila Jan 1, 2026
04f2c9e
Refactor AuthService test mocks for clarity
danny-avila Jan 1, 2026
04b2e45
docs: update .env.example
busla Jan 7, 2026
50ba642
πŸ› fix: Correct function signature in openid lax cookie tests
busla Jan 13, 2026
9dc720a
feat: add OPENID_EXPOSE_SUB_COOKIE for lax SameSite OpenID sub claim …
busla Jan 19, 2026
14861e6
fix(tests): update setOpenIDAuthTokens calls to match new signature
busla Jan 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,10 @@ OPENID_AUTO_REDIRECT=false
OPENID_USE_PKCE=false
#Set to true to reuse openid tokens for authentication management instead of using the mongodb session and the custom refresh token.
OPENID_REUSE_TOKENS=
# Set to true to expose a JWT-signed cookie containing the OpenID `sub` claim with sameSite=lax.
# This enables cross-origin OAuth callback flows (e.g., AWS Bedrock AgentCore 3LO).
# Can be used independently of OPENID_REUSE_TOKENS.
OPENID_EXPOSE_SUB_COOKIE=
#By default, signing key verification results are cached in order to prevent excessive HTTP requests to the JWKS endpoint.
#If a signing key matching the kid is found, this will be cached and the next time this kid is requested the signing key will be served from the cache.
#Default is true.
Expand Down
4 changes: 4 additions & 0 deletions api/cache/getLogStores.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ const namespaces = {
CacheKeys.OPENID_EXCHANGED_TOKENS,
Time.TEN_MINUTES,
),
[CacheKeys.ADMIN_OAUTH_EXCHANGE]: standardCache(
CacheKeys.ADMIN_OAUTH_EXCHANGE,
Time.THIRTY_SECONDS,
),
};

/**
Expand Down
16 changes: 11 additions & 5 deletions api/models/Agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,10 +589,16 @@ const deleteAgent = async (searchParameter) => {
const agent = await Agent.findOneAndDelete(searchParameter);
if (agent) {
await removeAgentFromAllProjects(agent.id);
await removeAllPermissions({
resourceType: ResourceType.AGENT,
resourceId: agent._id,
});
await Promise.all([
removeAllPermissions({
resourceType: ResourceType.AGENT,
resourceId: agent._id,
}),
removeAllPermissions({
resourceType: ResourceType.REMOTE_AGENT,
resourceId: agent._id,
}),
]);
try {
await Agent.updateMany({ 'edges.to': agent.id }, { $pull: { edges: { to: agent.id } } });
} catch (error) {
Expand Down Expand Up @@ -631,7 +637,7 @@ const deleteUserAgents = async (userId) => {
}

await AclEntry.deleteMany({
resourceType: ResourceType.AGENT,
resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] },
resourceId: { $in: agentObjectIds },
});

Expand Down
89 changes: 80 additions & 9 deletions api/models/File.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const getFiles = async (filter, _sortOptions, selectFields = { text: 0 }) => {
};

/**
* Retrieves tool files (files that are embedded or have a fileIdentifier) from an array of file IDs
* Retrieves tool files (files that are embedded or have a fileIdentifier) from an array of file IDs.
* Note: execute_code files are handled separately by getCodeGeneratedFiles.
* @param {string[]} fileIds - Array of file_id strings to search for
* @param {Set<EToolResources>} toolResourceSet - Optional filter for tool resources
* @returns {Promise<Array<MongoFile>>} Files that match the criteria
Expand All @@ -37,21 +38,25 @@ const getToolFilesByIds = async (fileIds, toolResourceSet) => {
}

try {
const filter = {
file_id: { $in: fileIds },
$or: [],
};
const orConditions = [];

if (toolResourceSet.has(EToolResources.context)) {
filter.$or.push({ text: { $exists: true, $ne: null }, context: FileContext.agents });
orConditions.push({ text: { $exists: true, $ne: null }, context: FileContext.agents });
}
if (toolResourceSet.has(EToolResources.file_search)) {
filter.$or.push({ embedded: true });
orConditions.push({ embedded: true });
}
if (toolResourceSet.has(EToolResources.execute_code)) {
filter.$or.push({ 'metadata.fileIdentifier': { $exists: true } });

if (orConditions.length === 0) {
return [];
}

const filter = {
file_id: { $in: fileIds },
context: { $ne: FileContext.execute_code }, // Exclude code-generated files
$or: orConditions,
};

const selectFields = { text: 0 };
const sortOptions = { updatedAt: -1 };

Expand All @@ -62,6 +67,70 @@ const getToolFilesByIds = async (fileIds, toolResourceSet) => {
}
};

/**
* Retrieves files generated by code execution for a given conversation.
* These files are stored locally with fileIdentifier metadata for code env re-upload.
* @param {string} conversationId - The conversation ID to search for
* @param {string[]} [messageIds] - Optional array of messageIds to filter by (for linear thread filtering)
* @returns {Promise<Array<MongoFile>>} Files generated by code execution in the conversation
*/
const getCodeGeneratedFiles = async (conversationId, messageIds) => {
if (!conversationId) {
return [];
}

/** messageIds are required for proper thread filtering of code-generated files */
if (!messageIds || messageIds.length === 0) {
return [];
}

try {
const filter = {
conversationId,
context: FileContext.execute_code,
messageId: { $exists: true, $in: messageIds },
'metadata.fileIdentifier': { $exists: true },
};

const selectFields = { text: 0 };
const sortOptions = { createdAt: 1 };

return await getFiles(filter, sortOptions, selectFields);
} catch (error) {
logger.error('[getCodeGeneratedFiles] Error retrieving code generated files:', error);
return [];
}
};

/**
* Retrieves user-uploaded execute_code files (not code-generated) by their file IDs.
* These are files with fileIdentifier metadata but context is NOT execute_code (e.g., agents or message_attachment).
* File IDs should be collected from message.files arrays in the current thread.
* @param {string[]} fileIds - Array of file IDs to fetch (from message.files in the thread)
* @returns {Promise<Array<MongoFile>>} User-uploaded execute_code files
*/
const getUserCodeFiles = async (fileIds) => {
if (!fileIds || fileIds.length === 0) {
return [];
}

try {
const filter = {
file_id: { $in: fileIds },
context: { $ne: FileContext.execute_code },
'metadata.fileIdentifier': { $exists: true },
};

const selectFields = { text: 0 };
const sortOptions = { createdAt: 1 };

return await getFiles(filter, sortOptions, selectFields);
} catch (error) {
logger.error('[getUserCodeFiles] Error retrieving user code files:', error);
return [];
}
};

/**
* Creates a new file with a TTL of 1 hour.
* @param {MongoFile} data - The file data to be created, must contain file_id.
Expand Down Expand Up @@ -169,6 +238,8 @@ module.exports = {
findFileById,
getFiles,
getToolFilesByIds,
getCodeGeneratedFiles,
getUserCodeFiles,
createFile,
updateFile,
updateFileUsage,
Expand Down
4 changes: 2 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.71.0",
"@anthropic-ai/vertex-sdk": "^0.14.0",
"@aws-sdk/client-bedrock-runtime": "^3.941.0",
"@aws-sdk/client-bedrock-runtime": "^3.970.0",
"@aws-sdk/client-s3": "^3.758.0",
"@aws-sdk/s3-request-presigner": "^3.758.0",
"@azure/identity": "^4.7.0",
Expand All @@ -45,7 +45,7 @@
"@google/genai": "^1.19.0",
"@keyv/redis": "^4.3.3",
"@langchain/core": "^0.3.80",
"@librechat/agents": "^3.0.776",
"@librechat/agents": "^3.1.0",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",
Expand Down
12 changes: 10 additions & 2 deletions api/server/controllers/PermissionsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
const mongoose = require('mongoose');
const { logger } = require('@librechat/data-schemas');
const { ResourceType, PrincipalType, PermissionBits } = require('librechat-data-provider');
const { enrichRemoteAgentPrincipals, backfillRemoteAgentPermissions } = require('@librechat/api');
const {
bulkUpdateResourcePermissions,
ensureGroupPrincipalExists,
Expand All @@ -14,7 +15,6 @@ const {
findAccessibleResources,
getResourcePermissionsMap,
} = require('~/server/services/PermissionService');
const { AclEntry } = require('~/db/models');
const {
searchPrincipals: searchLocalPrincipals,
sortPrincipalsByRelevance,
Expand All @@ -24,6 +24,7 @@ const {
entraIdPrincipalFeatureEnabled,
searchEntraIdPrincipals,
} = require('~/server/services/GraphApiService');
const { AclEntry, AccessRole } = require('~/db/models');

/**
* Generic controller for resource permission endpoints
Expand Down Expand Up @@ -234,7 +235,7 @@ const getResourcePermissions = async (req, res) => {
},
]);

const principals = [];
let principals = [];
let publicPermission = null;

// Process aggregation results
Expand Down Expand Up @@ -280,6 +281,13 @@ const getResourcePermissions = async (req, res) => {
}
}

if (resourceType === ResourceType.REMOTE_AGENT) {
const enricherDeps = { AclEntry, AccessRole, logger };
const enrichResult = await enrichRemoteAgentPrincipals(enricherDeps, resourceId, principals);
principals = enrichResult.principals;
backfillRemoteAgentPermissions(enricherDeps, resourceId, enrichResult.entriesToBackfill);
}

// Return response in format expected by frontend
const response = {
resourceType,
Expand Down
2 changes: 2 additions & 0 deletions api/server/controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const {
} = require('~/models');
const {
ConversationTag,
AgentApiKey,
Transaction,
MemoryEntry,
Assistant,
Expand Down Expand Up @@ -256,6 +257,7 @@ const deleteUserController = async (req, res) => {
await deleteFiles(null, user.id); // delete database files in case of orphaned files from previous steps
await deleteToolCalls(user.id); // delete user tool calls
await deleteUserAgents(user.id); // delete user agents
await AgentApiKey.deleteMany({ user: user._id }); // delete user agent API keys
await Assistant.deleteMany({ user: user.id }); // delete user assistants
await ConversationTag.deleteMany({ user: user.id }); // delete user conversation tags
await MemoryEntry.deleteMany({ userId: user.id }); // delete user memory entries
Expand Down
4 changes: 1 addition & 3 deletions api/server/controllers/agents/__tests__/callbacks.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ jest.mock('@librechat/data-schemas', () => ({
}));

jest.mock('@librechat/agents', () => ({
EnvVar: { CODE_API_KEY: 'CODE_API_KEY' },
Providers: { GOOGLE: 'google' },
GraphEvents: {},
...jest.requireActual('@librechat/agents'),
getMessageId: jest.fn(),
ToolEndHandler: jest.fn(),
handleToolCalls: jest.fn(),
Expand Down
Loading
Loading