🗑️ chore: Remove Deprecated Project Model and Associated Fields (#11773)

* chore: remove projects and projectIds usage

* chore: empty line linting

* chore: remove isCollaborative property across agent models and related tests

- Removed the isCollaborative property from agent models, controllers, and tests, as it is deprecated in favor of ACL permissions.
- Updated related validation schemas and data provider types to reflect this change.
- Ensured all references to isCollaborative were stripped from the codebase to maintain consistency and clarity.
This commit is contained in:
Danny Avila 2026-02-13 03:04:15 -05:00
parent 0efa41a518
commit 30df7b2ba5
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
41 changed files with 94 additions and 821 deletions

View file

@ -4,7 +4,6 @@ const { logger } = require('@librechat/data-schemas');
const { getCustomEndpointConfig } = require('@librechat/api'); const { getCustomEndpointConfig } = require('@librechat/api');
const { const {
Tools, Tools,
SystemRoles,
ResourceType, ResourceType,
actionDelimiter, actionDelimiter,
isAgentsEndpoint, isAgentsEndpoint,
@ -12,11 +11,6 @@ const {
encodeEphemeralAgentId, encodeEphemeralAgentId,
} = require('librechat-data-provider'); } = require('librechat-data-provider');
const { mcp_all, mcp_delimiter } = require('librechat-data-provider').Constants; const { mcp_all, mcp_delimiter } = require('librechat-data-provider').Constants;
const {
removeAgentFromAllProjects,
removeAgentIdsFromProject,
addAgentIdsToProject,
} = require('./Project');
const { removeAllPermissions } = require('~/server/services/PermissionService'); const { removeAllPermissions } = require('~/server/services/PermissionService');
const { getMCPServerTools } = require('~/server/services/Config'); const { getMCPServerTools } = require('~/server/services/Config');
const { Agent, AclEntry, User } = require('~/db/models'); const { Agent, AclEntry, User } = require('~/db/models');
@ -291,22 +285,8 @@ const isDuplicateVersion = (updateData, currentData, versions, actionsHash = nul
break; break;
} }
// Special handling for projectIds (MongoDB ObjectIds)
if (field === 'projectIds') {
const wouldBeIds = wouldBeArr.map((id) => id.toString()).sort();
const versionIds = lastVersionArr.map((id) => id.toString()).sort();
if (!wouldBeIds.every((id, i) => id === versionIds[i])) {
isMatch = false;
break;
}
}
// Handle arrays of objects // Handle arrays of objects
else if ( if (wouldBeArr.length > 0 && typeof wouldBeArr[0] === 'object' && wouldBeArr[0] !== null) {
wouldBeArr.length > 0 &&
typeof wouldBeArr[0] === 'object' &&
wouldBeArr[0] !== null
) {
const sortedWouldBe = [...wouldBeArr].map((item) => JSON.stringify(item)).sort(); const sortedWouldBe = [...wouldBeArr].map((item) => JSON.stringify(item)).sort();
const sortedVersion = [...lastVersionArr].map((item) => JSON.stringify(item)).sort(); const sortedVersion = [...lastVersionArr].map((item) => JSON.stringify(item)).sort();
@ -587,7 +567,6 @@ const removeAgentResourceFiles = async ({ agent_id, files }) => {
const deleteAgent = async (searchParameter) => { const deleteAgent = async (searchParameter) => {
const agent = await Agent.findOneAndDelete(searchParameter); const agent = await Agent.findOneAndDelete(searchParameter);
if (agent) { if (agent) {
await removeAgentFromAllProjects(agent.id);
await Promise.all([ await Promise.all([
removeAllPermissions({ removeAllPermissions({
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
@ -631,10 +610,6 @@ const deleteUserAgents = async (userId) => {
const agentIds = userAgents.map((agent) => agent.id); const agentIds = userAgents.map((agent) => agent.id);
const agentObjectIds = userAgents.map((agent) => agent._id); const agentObjectIds = userAgents.map((agent) => agent._id);
for (const agentId of agentIds) {
await removeAgentFromAllProjects(agentId);
}
await AclEntry.deleteMany({ await AclEntry.deleteMany({
resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] }, resourceType: { $in: [ResourceType.AGENT, ResourceType.REMOTE_AGENT] },
resourceId: { $in: agentObjectIds }, resourceId: { $in: agentObjectIds },
@ -710,7 +685,6 @@ const getListAgentsByAccess = async ({
name: 1, name: 1,
avatar: 1, avatar: 1,
author: 1, author: 1,
projectIds: 1,
description: 1, description: 1,
updatedAt: 1, updatedAt: 1,
category: 1, category: 1,
@ -755,64 +729,6 @@ const getListAgentsByAccess = async ({
}; };
}; };
/**
* Updates the projects associated with an agent, adding and removing project IDs as specified.
* This function also updates the corresponding projects to include or exclude the agent ID.
*
* @param {Object} params - Parameters for updating the agent's projects.
* @param {IUser} params.user - Parameters for updating the agent's projects.
* @param {string} params.agentId - The ID of the agent to update.
* @param {string[]} [params.projectIds] - Array of project IDs to add to the agent.
* @param {string[]} [params.removeProjectIds] - Array of project IDs to remove from the agent.
* @returns {Promise<MongoAgent>} The updated agent document.
* @throws {Error} If there's an error updating the agent or projects.
*/
const updateAgentProjects = async ({ user, agentId, projectIds, removeProjectIds }) => {
const updateOps = {};
if (removeProjectIds && removeProjectIds.length > 0) {
for (const projectId of removeProjectIds) {
await removeAgentIdsFromProject(projectId, [agentId]);
}
updateOps.$pullAll = { projectIds: removeProjectIds };
}
if (projectIds && projectIds.length > 0) {
for (const projectId of projectIds) {
await addAgentIdsToProject(projectId, [agentId]);
}
updateOps.$addToSet = { projectIds: { $each: projectIds } };
}
if (Object.keys(updateOps).length === 0) {
return await getAgent({ id: agentId });
}
const updateQuery = { id: agentId, author: user.id };
if (user.role === SystemRoles.ADMIN) {
delete updateQuery.author;
}
const updatedAgent = await updateAgent(updateQuery, updateOps, {
updatingUserId: user.id,
skipVersioning: true,
});
if (updatedAgent) {
return updatedAgent;
}
if (updateOps.$addToSet) {
for (const projectId of projectIds) {
await removeAgentIdsFromProject(projectId, [agentId]);
}
} else if (updateOps.$pull) {
for (const projectId of removeProjectIds) {
await addAgentIdsToProject(projectId, [agentId]);
}
}
return await getAgent({ id: agentId });
};
/** /**
* Reverts an agent to a specific version in its version history. * Reverts an agent to a specific version in its version history.
* @param {Object} searchParameter - The search parameters to find the agent to revert. * @param {Object} searchParameter - The search parameters to find the agent to revert.
@ -921,7 +837,6 @@ module.exports = {
deleteAgent, deleteAgent,
deleteUserAgents, deleteUserAgents,
revertAgentVersion, revertAgentVersion,
updateAgentProjects,
countPromotedAgents, countPromotedAgents,
addAgentResourceFile, addAgentResourceFile,
getListAgentsByAccess, getListAgentsByAccess,

View file

@ -24,7 +24,6 @@ const {
deleteAgent, deleteAgent,
deleteUserAgents, deleteUserAgents,
revertAgentVersion, revertAgentVersion,
updateAgentProjects,
addAgentResourceFile, addAgentResourceFile,
getListAgentsByAccess, getListAgentsByAccess,
removeAgentResourceFiles, removeAgentResourceFiles,
@ -1060,53 +1059,6 @@ describe('models/Agent', () => {
expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true); expect(userAfter.favorites.some((f) => f.model === 'gpt-4')).toBe(true);
}); });
test('should update agent projects', async () => {
const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId();
const projectId1 = new mongoose.Types.ObjectId();
const projectId2 = new mongoose.Types.ObjectId();
const projectId3 = new mongoose.Types.ObjectId();
await createAgent({
id: agentId,
name: 'Project Test Agent',
provider: 'test',
model: 'test-model',
author: authorId,
projectIds: [projectId1],
});
await updateAgent(
{ id: agentId },
{ $addToSet: { projectIds: { $each: [projectId2, projectId3] } } },
);
await updateAgent({ id: agentId }, { $pull: { projectIds: projectId1 } });
await updateAgent({ id: agentId }, { projectIds: [projectId2, projectId3] });
const updatedAgent = await getAgent({ id: agentId });
expect(updatedAgent.projectIds).toHaveLength(2);
expect(updatedAgent.projectIds.map((id) => id.toString())).toContain(projectId2.toString());
expect(updatedAgent.projectIds.map((id) => id.toString())).toContain(projectId3.toString());
expect(updatedAgent.projectIds.map((id) => id.toString())).not.toContain(
projectId1.toString(),
);
await updateAgent({ id: agentId }, { projectIds: [] });
const emptyProjectsAgent = await getAgent({ id: agentId });
expect(emptyProjectsAgent.projectIds).toHaveLength(0);
const nonExistentId = `agent_${uuidv4()}`;
await expect(
updateAgentProjects({
id: nonExistentId,
projectIds: [projectId1],
}),
).rejects.toThrow();
});
test('should handle ephemeral agent loading', async () => { test('should handle ephemeral agent loading', async () => {
const agentId = 'ephemeral_test'; const agentId = 'ephemeral_test';
const endpoint = 'openai'; const endpoint = 'openai';
@ -1178,20 +1130,6 @@ describe('models/Agent', () => {
const result = await fn(); const result = await fn();
expect(result).toBe(expected); expect(result).toBe(expected);
}); });
test('should handle updateAgentProjects with non-existent agent', async () => {
const nonExistentId = `agent_${uuidv4()}`;
const userId = new mongoose.Types.ObjectId();
const projectId = new mongoose.Types.ObjectId();
const result = await updateAgentProjects({
user: { id: userId.toString() },
agentId: nonExistentId,
projectIds: [projectId.toString()],
});
expect(result).toBeNull();
});
}); });
}); });
@ -1315,7 +1253,6 @@ describe('models/Agent', () => {
test('should handle MongoDB operators and field updates correctly', async () => { test('should handle MongoDB operators and field updates correctly', async () => {
const agentId = `agent_${uuidv4()}`; const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId(); const authorId = new mongoose.Types.ObjectId();
const projectId = new mongoose.Types.ObjectId();
await createAgent({ await createAgent({
id: agentId, id: agentId,
@ -1331,7 +1268,6 @@ describe('models/Agent', () => {
{ {
description: 'Updated description', description: 'Updated description',
$push: { tools: 'tool2' }, $push: { tools: 'tool2' },
$addToSet: { projectIds: projectId },
}, },
); );
@ -1339,7 +1275,6 @@ describe('models/Agent', () => {
expect(firstUpdate.description).toBe('Updated description'); expect(firstUpdate.description).toBe('Updated description');
expect(firstUpdate.tools).toContain('tool1'); expect(firstUpdate.tools).toContain('tool1');
expect(firstUpdate.tools).toContain('tool2'); expect(firstUpdate.tools).toContain('tool2');
expect(firstUpdate.projectIds.map((id) => id.toString())).toContain(projectId.toString());
expect(firstUpdate.versions).toHaveLength(2); expect(firstUpdate.versions).toHaveLength(2);
await updateAgent( await updateAgent(
@ -1744,7 +1679,6 @@ describe('models/Agent', () => {
test('should handle version comparison with special field types', async () => { test('should handle version comparison with special field types', async () => {
const agentId = `agent_${uuidv4()}`; const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId(); const authorId = new mongoose.Types.ObjectId();
const projectId = new mongoose.Types.ObjectId();
await createAgent({ await createAgent({
id: agentId, id: agentId,
@ -1752,7 +1686,6 @@ describe('models/Agent', () => {
provider: 'test', provider: 'test',
model: 'test-model', model: 'test-model',
author: authorId, author: authorId,
projectIds: [projectId],
model_parameters: { temperature: 0.7 }, model_parameters: { temperature: 0.7 },
}); });
@ -2630,7 +2563,6 @@ describe('models/Agent', () => {
const authorId = new mongoose.Types.ObjectId(); const authorId = new mongoose.Types.ObjectId();
const userId = new mongoose.Types.ObjectId(); const userId = new mongoose.Types.ObjectId();
const agentId = `agent_${uuidv4()}`; const agentId = `agent_${uuidv4()}`;
const projectId = new mongoose.Types.ObjectId();
await createAgent({ await createAgent({
id: agentId, id: agentId,
@ -2638,7 +2570,6 @@ describe('models/Agent', () => {
provider: 'openai', provider: 'openai',
model: 'gpt-4', model: 'gpt-4',
author: authorId, author: authorId,
projectIds: [projectId],
}); });
const mockReq = { user: { id: userId.toString() } }; const mockReq = { user: { id: userId.toString() } };
@ -2698,7 +2629,6 @@ describe('models/Agent', () => {
test('should handle agent creation with all optional fields', async () => { test('should handle agent creation with all optional fields', async () => {
const agentId = `agent_${uuidv4()}`; const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId(); const authorId = new mongoose.Types.ObjectId();
const projectId = new mongoose.Types.ObjectId();
const agent = await createAgent({ const agent = await createAgent({
id: agentId, id: agentId,
@ -2711,9 +2641,7 @@ describe('models/Agent', () => {
tools: ['tool1', 'tool2'], tools: ['tool1', 'tool2'],
actions: ['action1', 'action2'], actions: ['action1', 'action2'],
model_parameters: { temperature: 0.8, max_tokens: 1000 }, model_parameters: { temperature: 0.8, max_tokens: 1000 },
projectIds: [projectId],
avatar: 'https://example.com/avatar.png', avatar: 'https://example.com/avatar.png',
isCollaborative: true,
tool_resources: { tool_resources: {
file_search: { file_ids: ['file1', 'file2'] }, file_search: { file_ids: ['file1', 'file2'] },
}, },
@ -2727,9 +2655,7 @@ describe('models/Agent', () => {
expect(agent.actions).toEqual(['action1', 'action2']); expect(agent.actions).toEqual(['action1', 'action2']);
expect(agent.model_parameters.temperature).toBe(0.8); expect(agent.model_parameters.temperature).toBe(0.8);
expect(agent.model_parameters.max_tokens).toBe(1000); expect(agent.model_parameters.max_tokens).toBe(1000);
expect(agent.projectIds.map((id) => id.toString())).toContain(projectId.toString());
expect(agent.avatar).toBe('https://example.com/avatar.png'); expect(agent.avatar).toBe('https://example.com/avatar.png');
expect(agent.isCollaborative).toBe(true);
expect(agent.tool_resources.file_search.file_ids).toEqual(['file1', 'file2']); expect(agent.tool_resources.file_search.file_ids).toEqual(['file1', 'file2']);
}); });
@ -2935,21 +2861,6 @@ describe('models/Agent', () => {
expect(finalAgent.name).toBe('Version 4'); expect(finalAgent.name).toBe('Version 4');
}); });
test('should handle updateAgentProjects error scenarios', async () => {
const nonExistentId = `agent_${uuidv4()}`;
const userId = new mongoose.Types.ObjectId();
const projectId = new mongoose.Types.ObjectId();
// Test with non-existent agent
const result = await updateAgentProjects({
user: { id: userId.toString() },
agentId: nonExistentId,
projectIds: [projectId.toString()],
});
expect(result).toBeNull();
});
test('should handle revertAgentVersion properly', async () => { test('should handle revertAgentVersion properly', async () => {
const agentId = `agent_${uuidv4()}`; const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId(); const authorId = new mongoose.Types.ObjectId();
@ -3003,8 +2914,6 @@ describe('models/Agent', () => {
test('should handle updateAgent with combined MongoDB operators', async () => { test('should handle updateAgent with combined MongoDB operators', async () => {
const agentId = `agent_${uuidv4()}`; const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId(); const authorId = new mongoose.Types.ObjectId();
const projectId1 = new mongoose.Types.ObjectId();
const projectId2 = new mongoose.Types.ObjectId();
await createAgent({ await createAgent({
id: agentId, id: agentId,
@ -3013,7 +2922,6 @@ describe('models/Agent', () => {
model: 'test-model', model: 'test-model',
author: authorId, author: authorId,
tools: ['tool1'], tools: ['tool1'],
projectIds: [projectId1],
}); });
// Use multiple operators in single update - but avoid conflicting operations on same field // Use multiple operators in single update - but avoid conflicting operations on same field
@ -3022,14 +2930,6 @@ describe('models/Agent', () => {
{ {
name: 'Updated Name', name: 'Updated Name',
$push: { tools: 'tool2' }, $push: { tools: 'tool2' },
$addToSet: { projectIds: projectId2 },
},
);
const finalAgent = await updateAgent(
{ id: agentId },
{
$pull: { projectIds: projectId1 },
}, },
); );
@ -3037,11 +2937,7 @@ describe('models/Agent', () => {
expect(updatedAgent.name).toBe('Updated Name'); expect(updatedAgent.name).toBe('Updated Name');
expect(updatedAgent.tools).toContain('tool1'); expect(updatedAgent.tools).toContain('tool1');
expect(updatedAgent.tools).toContain('tool2'); expect(updatedAgent.tools).toContain('tool2');
expect(updatedAgent.projectIds.map((id) => id.toString())).toContain(projectId2.toString()); expect(updatedAgent.versions).toHaveLength(2);
expect(finalAgent).toBeDefined();
expect(finalAgent.projectIds.map((id) => id.toString())).not.toContain(projectId1.toString());
expect(finalAgent.versions).toHaveLength(3);
}); });
test('should handle updateAgent when agent does not exist', async () => { test('should handle updateAgent when agent does not exist', async () => {
@ -3315,65 +3211,6 @@ describe('models/Agent', () => {
expect(updated2.description).toBe('Another description'); expect(updated2.description).toBe('Another description');
}); });
test('should skip version creation when skipVersioning option is used', async () => {
const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId();
const projectId1 = new mongoose.Types.ObjectId();
const projectId2 = new mongoose.Types.ObjectId();
// Create agent with initial projectIds
await createAgent({
id: agentId,
name: 'Test Agent',
provider: 'test',
model: 'test-model',
author: authorId,
projectIds: [projectId1],
});
// Share agent using updateAgentProjects (which uses skipVersioning)
const shared = await updateAgentProjects({
user: { id: authorId.toString() }, // Use the same author ID
agentId: agentId,
projectIds: [projectId2.toString()],
});
// Should NOT create a new version due to skipVersioning
expect(shared.versions).toHaveLength(1);
expect(shared.projectIds.map((id) => id.toString())).toContain(projectId1.toString());
expect(shared.projectIds.map((id) => id.toString())).toContain(projectId2.toString());
// Unshare agent using updateAgentProjects
const unshared = await updateAgentProjects({
user: { id: authorId.toString() },
agentId: agentId,
removeProjectIds: [projectId1.toString()],
});
// Still should NOT create a new version
expect(unshared.versions).toHaveLength(1);
expect(unshared.projectIds.map((id) => id.toString())).not.toContain(projectId1.toString());
expect(unshared.projectIds.map((id) => id.toString())).toContain(projectId2.toString());
// Regular update without skipVersioning should create a version
const regularUpdate = await updateAgent(
{ id: agentId },
{ description: 'Updated description' },
);
expect(regularUpdate.versions).toHaveLength(2);
expect(regularUpdate.description).toBe('Updated description');
// Direct updateAgent with MongoDB operators should still create versions
const directUpdate = await updateAgent(
{ id: agentId },
{ $addToSet: { projectIds: { $each: [projectId1] } } },
);
expect(directUpdate.versions).toHaveLength(3);
expect(directUpdate.projectIds.length).toBe(2);
});
test('should preserve agent_ids in version history', async () => { test('should preserve agent_ids in version history', async () => {
const agentId = `agent_${uuidv4()}`; const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId(); const authorId = new mongoose.Types.ObjectId();
@ -3754,7 +3591,6 @@ function createTestIds() {
return { return {
agentId: `agent_${uuidv4()}`, agentId: `agent_${uuidv4()}`,
authorId: new mongoose.Types.ObjectId(), authorId: new mongoose.Types.ObjectId(),
projectId: new mongoose.Types.ObjectId(),
fileId: uuidv4(), fileId: uuidv4(),
}; };
} }
@ -3788,9 +3624,6 @@ function mockFindOneAndUpdateError(errorOnCall = 1) {
} }
function generateVersionTestCases() { function generateVersionTestCases() {
const projectId1 = new mongoose.Types.ObjectId();
const projectId2 = new mongoose.Types.ObjectId();
return [ return [
{ {
name: 'simple field update', name: 'simple field update',
@ -3817,13 +3650,5 @@ function generateVersionTestCases() {
update: { tools: ['tool2', 'tool3'] }, update: { tools: ['tool2', 'tool3'] },
duplicate: { tools: ['tool2', 'tool3'] }, duplicate: { tools: ['tool2', 'tool3'] },
}, },
{
name: 'projectIds update',
initial: {
projectIds: [projectId1],
},
update: { projectIds: [projectId1, projectId2] },
duplicate: { projectIds: [projectId2, projectId1] },
},
]; ];
} }

View file

@ -1,133 +0,0 @@
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
const { Project } = require('~/db/models');
/**
* Retrieve a project by ID and convert the found project document to a plain object.
*
* @param {string} projectId - The ID of the project to find and return as a plain object.
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document.
* @returns {Promise<IMongoProject>} A plain object representing the project document, or `null` if no project is found.
*/
const getProjectById = async function (projectId, fieldsToSelect = null) {
const query = Project.findById(projectId);
if (fieldsToSelect) {
query.select(fieldsToSelect);
}
return await query.lean();
};
/**
* Retrieve a project by name and convert the found project document to a plain object.
* If the project with the given name doesn't exist and the name is "instance", create it and return the lean version.
*
* @param {string} projectName - The name of the project to find or create.
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document.
* @returns {Promise<IMongoProject>} A plain object representing the project document.
*/
const getProjectByName = async function (projectName, fieldsToSelect = null) {
const query = { name: projectName };
const update = { $setOnInsert: { name: projectName } };
const options = {
new: true,
upsert: projectName === GLOBAL_PROJECT_NAME,
lean: true,
select: fieldsToSelect,
};
return await Project.findOneAndUpdate(query, update, options);
};
/**
* Add an array of prompt group IDs to a project's promptGroupIds array, ensuring uniqueness.
*
* @param {string} projectId - The ID of the project to update.
* @param {string[]} promptGroupIds - The array of prompt group IDs to add to the project.
* @returns {Promise<IMongoProject>} The updated project document.
*/
const addGroupIdsToProject = async function (projectId, promptGroupIds) {
return await Project.findByIdAndUpdate(
projectId,
{ $addToSet: { promptGroupIds: { $each: promptGroupIds } } },
{ new: true },
);
};
/**
* Remove an array of prompt group IDs from a project's promptGroupIds array.
*
* @param {string} projectId - The ID of the project to update.
* @param {string[]} promptGroupIds - The array of prompt group IDs to remove from the project.
* @returns {Promise<IMongoProject>} The updated project document.
*/
const removeGroupIdsFromProject = async function (projectId, promptGroupIds) {
return await Project.findByIdAndUpdate(
projectId,
{ $pullAll: { promptGroupIds: promptGroupIds } },
{ new: true },
);
};
/**
* Remove a prompt group ID from all projects.
*
* @param {string} promptGroupId - The ID of the prompt group to remove from projects.
* @returns {Promise<void>}
*/
const removeGroupFromAllProjects = async (promptGroupId) => {
await Project.updateMany({}, { $pullAll: { promptGroupIds: [promptGroupId] } });
};
/**
* Add an array of agent IDs to a project's agentIds array, ensuring uniqueness.
*
* @param {string} projectId - The ID of the project to update.
* @param {string[]} agentIds - The array of agent IDs to add to the project.
* @returns {Promise<IMongoProject>} The updated project document.
*/
const addAgentIdsToProject = async function (projectId, agentIds) {
return await Project.findByIdAndUpdate(
projectId,
{ $addToSet: { agentIds: { $each: agentIds } } },
{ new: true },
);
};
/**
* Remove an array of agent IDs from a project's agentIds array.
*
* @param {string} projectId - The ID of the project to update.
* @param {string[]} agentIds - The array of agent IDs to remove from the project.
* @returns {Promise<IMongoProject>} The updated project document.
*/
const removeAgentIdsFromProject = async function (projectId, agentIds) {
return await Project.findByIdAndUpdate(
projectId,
{ $pullAll: { agentIds: agentIds } },
{ new: true },
);
};
/**
* Remove an agent ID from all projects.
*
* @param {string} agentId - The ID of the agent to remove from projects.
* @returns {Promise<void>}
*/
const removeAgentFromAllProjects = async (agentId) => {
await Project.updateMany({}, { $pullAll: { agentIds: [agentId] } });
};
module.exports = {
getProjectById,
getProjectByName,
/* prompts */
addGroupIdsToProject,
removeGroupIdsFromProject,
removeGroupFromAllProjects,
/* agents */
addAgentIdsToProject,
removeAgentIdsFromProject,
removeAgentFromAllProjects,
};

View file

@ -1,18 +1,7 @@
const { ObjectId } = require('mongodb'); const { ObjectId } = require('mongodb');
const { escapeRegExp } = require('@librechat/api'); const { escapeRegExp } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
const { const { SystemRoles, ResourceType, SystemCategories } = require('librechat-data-provider');
Constants,
SystemRoles,
ResourceType,
SystemCategories,
} = require('librechat-data-provider');
const {
removeGroupFromAllProjects,
removeGroupIdsFromProject,
addGroupIdsToProject,
getProjectByName,
} = require('./Project');
const { removeAllPermissions } = require('~/server/services/PermissionService'); const { removeAllPermissions } = require('~/server/services/PermissionService');
const { PromptGroup, Prompt, AclEntry } = require('~/db/models'); const { PromptGroup, Prompt, AclEntry } = require('~/db/models');
@ -48,34 +37,21 @@ const getAllPromptGroups = async (req, filter) => {
try { try {
const { name, ...query } = filter; const { name, ...query } = filter;
let searchShared = true;
let searchSharedOnly = false;
if (name) { if (name) {
query.name = new RegExp(escapeRegExp(name), 'i'); query.name = new RegExp(escapeRegExp(name), 'i');
} }
if (!query.category) { if (!query.category) {
delete query.category; delete query.category;
} else if (query.category === SystemCategories.MY_PROMPTS) { } else if (query.category === SystemCategories.MY_PROMPTS) {
searchShared = false;
delete query.category; delete query.category;
} else if (query.category === SystemCategories.NO_CATEGORY) { } else if (query.category === SystemCategories.NO_CATEGORY) {
query.category = ''; query.category = '';
} else if (query.category === SystemCategories.SHARED_PROMPTS) { } else if (query.category === SystemCategories.SHARED_PROMPTS) {
searchSharedOnly = true;
delete query.category; delete query.category;
} }
let combinedQuery = query; let combinedQuery = query;
if (searchShared) {
const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds');
if (project && project.promptGroupIds && project.promptGroupIds.length > 0) {
const projectQuery = { _id: { $in: project.promptGroupIds }, ...query };
delete projectQuery.author;
combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] };
}
}
const groups = await PromptGroup.find(combinedQuery) const groups = await PromptGroup.find(combinedQuery)
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.select('name oneliner category author authorName createdAt updatedAt command productionId') .select('name oneliner category author authorName createdAt updatedAt command productionId')
@ -100,34 +76,21 @@ const getPromptGroups = async (req, filter) => {
const validatedPageNumber = Math.max(parseInt(pageNumber, 10), 1); const validatedPageNumber = Math.max(parseInt(pageNumber, 10), 1);
const validatedPageSize = Math.max(parseInt(pageSize, 10), 1); const validatedPageSize = Math.max(parseInt(pageSize, 10), 1);
let searchShared = true;
let searchSharedOnly = false;
if (name) { if (name) {
query.name = new RegExp(escapeRegExp(name), 'i'); query.name = new RegExp(escapeRegExp(name), 'i');
} }
if (!query.category) { if (!query.category) {
delete query.category; delete query.category;
} else if (query.category === SystemCategories.MY_PROMPTS) { } else if (query.category === SystemCategories.MY_PROMPTS) {
searchShared = false;
delete query.category; delete query.category;
} else if (query.category === SystemCategories.NO_CATEGORY) { } else if (query.category === SystemCategories.NO_CATEGORY) {
query.category = ''; query.category = '';
} else if (query.category === SystemCategories.SHARED_PROMPTS) { } else if (query.category === SystemCategories.SHARED_PROMPTS) {
searchSharedOnly = true;
delete query.category; delete query.category;
} }
let combinedQuery = query; let combinedQuery = query;
if (searchShared) {
const project = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, 'promptGroupIds');
if (project && project.promptGroupIds && project.promptGroupIds.length > 0) {
const projectQuery = { _id: { $in: project.promptGroupIds }, ...query };
delete projectQuery.author;
combinedQuery = searchSharedOnly ? projectQuery : { $or: [projectQuery, query] };
}
}
const skip = (validatedPageNumber - 1) * validatedPageSize; const skip = (validatedPageNumber - 1) * validatedPageSize;
const limit = validatedPageSize; const limit = validatedPageSize;
@ -137,7 +100,7 @@ const getPromptGroups = async (req, filter) => {
.skip(skip) .skip(skip)
.limit(limit) .limit(limit)
.select( .select(
'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt',
) )
.lean(), .lean(),
PromptGroup.countDocuments(combinedQuery), PromptGroup.countDocuments(combinedQuery),
@ -182,7 +145,6 @@ const deletePromptGroup = async ({ _id, author, role }) => {
} }
await Prompt.deleteMany(groupQuery); await Prompt.deleteMany(groupQuery);
await removeGroupFromAllProjects(_id);
try { try {
await removeAllPermissions({ resourceType: ResourceType.PROMPTGROUP, resourceId: _id }); await removeAllPermissions({ resourceType: ResourceType.PROMPTGROUP, resourceId: _id });
@ -241,7 +203,7 @@ async function getListPromptGroupsByAccess({
const findQuery = PromptGroup.find(baseQuery) const findQuery = PromptGroup.find(baseQuery)
.sort({ updatedAt: -1, _id: 1 }) .sort({ updatedAt: -1, _id: 1 })
.select( .select(
'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt',
); );
if (isPaginated) { if (isPaginated) {
@ -487,7 +449,6 @@ module.exports = {
} }
await PromptGroup.deleteOne({ _id: groupId }); await PromptGroup.deleteOne({ _id: groupId });
await removeGroupFromAllProjects(groupId);
return { return {
prompt: 'Prompt deleted successfully', prompt: 'Prompt deleted successfully',
@ -523,10 +484,6 @@ module.exports = {
const groupIds = promptGroups.map((group) => group._id); const groupIds = promptGroups.map((group) => group._id);
for (const groupId of groupIds) {
await removeGroupFromAllProjects(groupId);
}
await AclEntry.deleteMany({ await AclEntry.deleteMany({
resourceType: ResourceType.PROMPTGROUP, resourceType: ResourceType.PROMPTGROUP,
resourceId: { $in: groupIds }, resourceId: { $in: groupIds },
@ -547,23 +504,6 @@ module.exports = {
updatePromptGroup: async (filter, data) => { updatePromptGroup: async (filter, data) => {
try { try {
const updateOps = {}; const updateOps = {};
if (data.removeProjectIds) {
for (const projectId of data.removeProjectIds) {
await removeGroupIdsFromProject(projectId, [filter._id]);
}
updateOps.$pullAll = { projectIds: data.removeProjectIds };
delete data.removeProjectIds;
}
if (data.projectIds) {
for (const projectId of data.projectIds) {
await addGroupIdsToProject(projectId, [filter._id]);
}
updateOps.$addToSet = { projectIds: { $each: data.projectIds } };
delete data.projectIds;
}
const updateData = { ...data, ...updateOps }; const updateData = { ...data, ...updateOps };
const updatedDoc = await PromptGroup.findOneAndUpdate(filter, updateData, { const updatedDoc = await PromptGroup.findOneAndUpdate(filter, updateData, {

View file

@ -19,7 +19,7 @@ const dbModels = require('~/db/models');
logger.silent = true; logger.silent = true;
let mongoServer; let mongoServer;
let Prompt, PromptGroup, AclEntry, AccessRole, User, Group, Project; let Prompt, PromptGroup, AclEntry, AccessRole, User, Group;
let promptFns, permissionService; let promptFns, permissionService;
let testUsers, testGroups, testRoles; let testUsers, testGroups, testRoles;
@ -36,7 +36,6 @@ beforeAll(async () => {
AccessRole = dbModels.AccessRole; AccessRole = dbModels.AccessRole;
User = dbModels.User; User = dbModels.User;
Group = dbModels.Group; Group = dbModels.Group;
Project = dbModels.Project;
promptFns = require('~/models/Prompt'); promptFns = require('~/models/Prompt');
permissionService = require('~/server/services/PermissionService'); permissionService = require('~/server/services/PermissionService');
@ -118,12 +117,6 @@ async function setupTestData() {
description: 'Group with viewer access', description: 'Group with viewer access',
}), }),
}; };
await Project.create({
name: 'Global',
description: 'Global project',
promptGroupIds: [],
});
} }
describe('Prompt ACL Permissions', () => { describe('Prompt ACL Permissions', () => {

View file

@ -3,7 +3,6 @@ const { ObjectId } = require('mongodb');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server'); const { MongoMemoryServer } = require('mongodb-memory-server');
const { const {
Constants,
ResourceType, ResourceType,
AccessRoleIds, AccessRoleIds,
PrincipalType, PrincipalType,
@ -19,9 +18,9 @@ logger.silent = true;
describe('PromptGroup Migration Script', () => { describe('PromptGroup Migration Script', () => {
let mongoServer; let mongoServer;
let Prompt, PromptGroup, AclEntry, AccessRole, User, Project; let Prompt, PromptGroup, AclEntry, AccessRole, User;
let migrateToPromptGroupPermissions; let migrateToPromptGroupPermissions;
let testOwner, testProject; let testOwner;
let ownerRole, viewerRole; let ownerRole, viewerRole;
beforeAll(async () => { beforeAll(async () => {
@ -37,7 +36,6 @@ describe('PromptGroup Migration Script', () => {
AclEntry = dbModels.AclEntry; AclEntry = dbModels.AclEntry;
AccessRole = dbModels.AccessRole; AccessRole = dbModels.AccessRole;
User = dbModels.User; User = dbModels.User;
Project = dbModels.Project;
// Create test user // Create test user
testOwner = await User.create({ testOwner = await User.create({
@ -46,11 +44,10 @@ describe('PromptGroup Migration Script', () => {
role: 'USER', role: 'USER',
}); });
// Create test project with the proper name // Create test project document in the raw `projects` collection
const projectName = Constants.GLOBAL_PROJECT_NAME || 'instance'; const projectName = 'instance';
testProject = await Project.create({ await mongoose.connection.db.collection('projects').insertOne({
name: projectName, name: projectName,
description: 'Global project',
promptGroupIds: [], promptGroupIds: [],
}); });
@ -95,9 +92,9 @@ describe('PromptGroup Migration Script', () => {
await Prompt.deleteMany({}); await Prompt.deleteMany({});
await PromptGroup.deleteMany({}); await PromptGroup.deleteMany({});
await AclEntry.deleteMany({}); await AclEntry.deleteMany({});
// Reset the project's promptGroupIds array await mongoose.connection.db
testProject.promptGroupIds = []; .collection('projects')
await testProject.save(); .updateOne({ name: 'instance' }, { $set: { promptGroupIds: [] } });
}); });
it('should categorize promptGroups correctly in dry run', async () => { it('should categorize promptGroups correctly in dry run', async () => {
@ -118,8 +115,9 @@ describe('PromptGroup Migration Script', () => {
}); });
// Add global group to project's promptGroupIds array // Add global group to project's promptGroupIds array
testProject.promptGroupIds = [globalPromptGroup._id]; await mongoose.connection.db
await testProject.save(); .collection('projects')
.updateOne({ name: 'instance' }, { $set: { promptGroupIds: [globalPromptGroup._id] } });
const result = await migrateToPromptGroupPermissions({ dryRun: true }); const result = await migrateToPromptGroupPermissions({ dryRun: true });
@ -146,8 +144,9 @@ describe('PromptGroup Migration Script', () => {
}); });
// Add global group to project's promptGroupIds array // Add global group to project's promptGroupIds array
testProject.promptGroupIds = [globalPromptGroup._id]; await mongoose.connection.db
await testProject.save(); .collection('projects')
.updateOne({ name: 'instance' }, { $set: { promptGroupIds: [globalPromptGroup._id] } });
const result = await migrateToPromptGroupPermissions({ dryRun: false }); const result = await migrateToPromptGroupPermissions({ dryRun: false });

View file

@ -173,9 +173,6 @@ const getAgentHandler = async (req, res, expandProperties = false) => {
agent.author = agent.author.toString(); agent.author = agent.author.toString();
// @deprecated - isCollaborative replaced by ACL permissions
agent.isCollaborative = !!agent.isCollaborative;
// Check if agent is public // Check if agent is public
const isPublic = await hasPublicPermission({ const isPublic = await hasPublicPermission({
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
@ -199,9 +196,6 @@ const getAgentHandler = async (req, res, expandProperties = false) => {
author: agent.author, author: agent.author,
provider: agent.provider, provider: agent.provider,
model: agent.model, model: agent.model,
projectIds: agent.projectIds,
// @deprecated - isCollaborative replaced by ACL permissions
isCollaborative: agent.isCollaborative,
isPublic: agent.isPublic, isPublic: agent.isPublic,
version: agent.version, version: agent.version,
// Safe metadata // Safe metadata

View file

@ -14,10 +14,6 @@ jest.mock('~/server/services/Config', () => ({
}), }),
})); }));
jest.mock('~/models/Project', () => ({
getProjectByName: jest.fn().mockResolvedValue(null),
}));
jest.mock('~/server/services/Files/strategies', () => ({ jest.mock('~/server/services/Files/strategies', () => ({
getStrategyFunctions: jest.fn(), getStrategyFunctions: jest.fn(),
})); }));
@ -175,7 +171,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
// Unauthorized fields that should be stripped // Unauthorized fields that should be stripped
author: new mongoose.Types.ObjectId().toString(), // Should not be able to set author author: new mongoose.Types.ObjectId().toString(), // Should not be able to set author
authorName: 'Hacker', // Should be stripped authorName: 'Hacker', // Should be stripped
isCollaborative: true, // Should be stripped on creation
versions: [], // Should be stripped versions: [], // Should be stripped
_id: new mongoose.Types.ObjectId(), // Should be stripped _id: new mongoose.Types.ObjectId(), // Should be stripped
id: 'custom_agent_id', // Should be overridden id: 'custom_agent_id', // Should be overridden
@ -194,7 +189,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
// Verify unauthorized fields were not set // Verify unauthorized fields were not set
expect(createdAgent.author.toString()).toBe(mockReq.user.id); // Should be the request user, not the malicious value expect(createdAgent.author.toString()).toBe(mockReq.user.id); // Should be the request user, not the malicious value
expect(createdAgent.authorName).toBeUndefined(); expect(createdAgent.authorName).toBeUndefined();
expect(createdAgent.isCollaborative).toBeFalsy();
expect(createdAgent.versions).toHaveLength(1); // Should have exactly 1 version from creation expect(createdAgent.versions).toHaveLength(1); // Should have exactly 1 version from creation
expect(createdAgent.id).not.toBe('custom_agent_id'); // Should have generated ID expect(createdAgent.id).not.toBe('custom_agent_id'); // Should have generated ID
expect(createdAgent.id).toMatch(/^agent_/); // Should have proper prefix expect(createdAgent.id).toMatch(/^agent_/); // Should have proper prefix
@ -445,7 +439,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
model: 'gpt-3.5-turbo', model: 'gpt-3.5-turbo',
author: existingAgentAuthorId, author: existingAgentAuthorId,
description: 'Original description', description: 'Original description',
isCollaborative: false,
versions: [ versions: [
{ {
name: 'Original Agent', name: 'Original Agent',
@ -467,7 +460,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
name: 'Updated Agent', name: 'Updated Agent',
description: 'Updated description', description: 'Updated description',
model: 'gpt-4', model: 'gpt-4',
isCollaborative: true, // This IS allowed in updates
}; };
await updateAgentHandler(mockReq, mockRes); await updateAgentHandler(mockReq, mockRes);
@ -480,13 +472,11 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
expect(updatedAgent.name).toBe('Updated Agent'); expect(updatedAgent.name).toBe('Updated Agent');
expect(updatedAgent.description).toBe('Updated description'); expect(updatedAgent.description).toBe('Updated description');
expect(updatedAgent.model).toBe('gpt-4'); expect(updatedAgent.model).toBe('gpt-4');
expect(updatedAgent.isCollaborative).toBe(true);
expect(updatedAgent.author).toBe(existingAgentAuthorId.toString()); expect(updatedAgent.author).toBe(existingAgentAuthorId.toString());
// Verify in database // Verify in database
const agentInDb = await Agent.findOne({ id: existingAgentId }); const agentInDb = await Agent.findOne({ id: existingAgentId });
expect(agentInDb.name).toBe('Updated Agent'); expect(agentInDb.name).toBe('Updated Agent');
expect(agentInDb.isCollaborative).toBe(true);
}); });
test('should reject update with unauthorized fields (mass assignment protection)', async () => { test('should reject update with unauthorized fields (mass assignment protection)', async () => {
@ -541,25 +531,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
expect(updatedAgent.name).toBe('Admin Update'); expect(updatedAgent.name).toBe('Admin Update');
}); });
test('should handle projectIds updates', async () => {
mockReq.user.id = existingAgentAuthorId.toString();
mockReq.params.id = existingAgentId;
const projectId1 = new mongoose.Types.ObjectId().toString();
const projectId2 = new mongoose.Types.ObjectId().toString();
mockReq.body = {
projectIds: [projectId1, projectId2],
};
await updateAgentHandler(mockReq, mockRes);
expect(mockRes.json).toHaveBeenCalled();
const updatedAgent = mockRes.json.mock.calls[0][0];
expect(updatedAgent).toBeDefined();
});
test('should validate tool_resources in updates', async () => { test('should validate tool_resources in updates', async () => {
mockReq.user.id = existingAgentAuthorId.toString(); mockReq.user.id = existingAgentAuthorId.toString();
mockReq.params.id = existingAgentId; mockReq.params.id = existingAgentId;

View file

@ -21,15 +21,6 @@ const checkAgentCreate = generateCheckAccess({
getRoleByName, getRoleByName,
}); });
const checkGlobalAgentShare = generateCheckAccess({
permissionType: PermissionTypes.AGENTS,
permissions: [Permissions.USE, Permissions.CREATE],
bodyProps: {
[Permissions.SHARE]: ['projectIds', 'removeProjectIds'],
},
getRoleByName,
});
router.use(requireJwtAuth); router.use(requireJwtAuth);
/** /**
@ -99,7 +90,7 @@ router.get(
*/ */
router.patch( router.patch(
'/:id', '/:id',
checkGlobalAgentShare, checkAgentCreate,
canAccessAgentResource({ canAccessAgentResource({
requiredPermission: PermissionBits.EDIT, requiredPermission: PermissionBits.EDIT,
resourceIdParam: 'id', resourceIdParam: 'id',
@ -148,7 +139,7 @@ router.delete(
*/ */
router.post( router.post(
'/:id/revert', '/:id/revert',
checkGlobalAgentShare, checkAgentCreate,
canAccessAgentResource({ canAccessAgentResource({
requiredPermission: PermissionBits.EDIT, requiredPermission: PermissionBits.EDIT,
resourceIdParam: 'id', resourceIdParam: 'id',

View file

@ -1,10 +1,9 @@
const express = require('express'); const express = require('express');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
const { isEnabled, getBalanceConfig } = require('@librechat/api'); const { isEnabled, getBalanceConfig } = require('@librechat/api');
const { Constants, CacheKeys, defaultSocialLogins } = require('librechat-data-provider'); const { CacheKeys, defaultSocialLogins } = require('librechat-data-provider');
const { getLdapConfig } = require('~/server/services/Config/ldap'); const { getLdapConfig } = require('~/server/services/Config/ldap');
const { getAppConfig } = require('~/server/services/Config/app'); const { getAppConfig } = require('~/server/services/Config/app');
const { getProjectByName } = require('~/models/Project');
const { getLogStores } = require('~/cache'); const { getLogStores } = require('~/cache');
const router = express.Router(); const router = express.Router();
@ -37,8 +36,6 @@ router.get('/', async function (req, res) {
return today.getMonth() === 1 && today.getDate() === 11; return today.getMonth() === 1 && today.getDate() === 11;
}; };
const instanceProject = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, '_id');
const ldap = getLdapConfig(); const ldap = getLdapConfig();
try { try {
@ -101,7 +98,6 @@ router.get('/', async function (req, res) {
sharedLinksEnabled, sharedLinksEnabled,
publicSharedLinksEnabled, publicSharedLinksEnabled,
analyticsGtmId: process.env.ANALYTICS_GTM_ID, analyticsGtmId: process.env.ANALYTICS_GTM_ID,
instanceProjectId: instanceProject._id.toString(),
bundlerURL: process.env.SANDPACK_BUNDLER_URL, bundlerURL: process.env.SANDPACK_BUNDLER_URL,
staticBundlerURL: process.env.SANDPACK_STATIC_BUNDLER_URL, staticBundlerURL: process.env.SANDPACK_STATIC_BUNDLER_URL,
sharePointFilePickerEnabled, sharePointFilePickerEnabled,

View file

@ -56,15 +56,6 @@ const checkPromptCreate = generateCheckAccess({
getRoleByName, getRoleByName,
}); });
const checkGlobalPromptShare = generateCheckAccess({
permissionType: PermissionTypes.PROMPTS,
permissions: [Permissions.USE, Permissions.CREATE],
bodyProps: {
[Permissions.SHARE]: ['projectIds', 'removeProjectIds'],
},
getRoleByName,
});
router.use(requireJwtAuth); router.use(requireJwtAuth);
router.use(checkPromptAccess); router.use(checkPromptAccess);
@ -364,7 +355,7 @@ const patchPromptGroup = async (req, res) => {
router.patch( router.patch(
'/groups/:groupId', '/groups/:groupId',
checkGlobalPromptShare, checkPromptCreate,
canAccessPromptGroupResource({ canAccessPromptGroupResource({
requiredPermission: PermissionBits.EDIT, requiredPermission: PermissionBits.EDIT,
}), }),

View file

@ -6,7 +6,6 @@ const {
checkAgentPermissionsMigration, checkAgentPermissionsMigration,
checkPromptPermissionsMigration, checkPromptPermissionsMigration,
} = require('@librechat/api'); } = require('@librechat/api');
const { getProjectByName } = require('~/models/Project');
const { Agent, PromptGroup } = require('~/db/models'); const { Agent, PromptGroup } = require('~/db/models');
const { findRoleByIdentifier } = require('~/models'); const { findRoleByIdentifier } = require('~/models');
@ -20,7 +19,6 @@ async function checkMigrations() {
mongoose, mongoose,
methods: { methods: {
findRoleByIdentifier, findRoleByIdentifier,
getProjectByName,
}, },
AgentModel: Agent, AgentModel: Agent,
}); });
@ -33,7 +31,6 @@ async function checkMigrations() {
mongoose, mongoose,
methods: { methods: {
findRoleByIdentifier, findRoleByIdentifier,
getProjectByName,
}, },
PromptGroupModel: PromptGroup, PromptGroupModel: PromptGroup,
}); });

View file

@ -16,22 +16,13 @@ import PreviewPrompt from '~/components/Prompts/PreviewPrompt';
import ListCard from '~/components/Prompts/Groups/ListCard'; import ListCard from '~/components/Prompts/Groups/ListCard';
import { detectVariables } from '~/utils'; import { detectVariables } from '~/utils';
function ChatGroupItem({ function ChatGroupItem({ group }: { group: TPromptGroup }) {
group,
instanceProjectId,
}: {
group: TPromptGroup;
instanceProjectId?: string;
}) {
const localize = useLocalize(); const localize = useLocalize();
const { submitPrompt } = useSubmitMessage(); const { submitPrompt } = useSubmitMessage();
const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false); const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false);
const [isVariableDialogOpen, setVariableDialogOpen] = useState(false); const [isVariableDialogOpen, setVariableDialogOpen] = useState(false);
const groupIsGlobal = useMemo( const groupIsGlobal = useMemo(() => group.isPublic === true, [group.isPublic]);
() => instanceProjectId != null && group.projectIds?.includes(instanceProjectId),
[group, instanceProjectId],
);
// Check permissions for the promptGroup // Check permissions for the promptGroup
const { hasPermission } = useResourcePermissions(ResourceType.PROMPTGROUP, group._id || ''); const { hasPermission } = useResourcePermissions(ResourceType.PROMPTGROUP, group._id || '');

View file

@ -19,10 +19,9 @@ import { cn } from '~/utils';
interface DashGroupItemProps { interface DashGroupItemProps {
group: TPromptGroup; group: TPromptGroup;
instanceProjectId?: string;
} }
function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps) { function DashGroupItemComponent({ group }: DashGroupItemProps) {
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const localize = useLocalize(); const localize = useLocalize();
@ -35,10 +34,7 @@ function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps
const canEdit = hasPermission(PermissionBits.EDIT); const canEdit = hasPermission(PermissionBits.EDIT);
const canDelete = hasPermission(PermissionBits.DELETE); const canDelete = hasPermission(PermissionBits.DELETE);
const isGlobalGroup = useMemo( const isPublicGroup = useMemo(() => group.isPublic === true, [group.isPublic]);
() => instanceProjectId && group.projectIds?.includes(instanceProjectId),
[group.projectIds, instanceProjectId],
);
const updateGroup = useUpdatePromptGroup({ const updateGroup = useUpdatePromptGroup({
onMutate: () => { onMutate: () => {
@ -115,7 +111,7 @@ function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps
</div> </div>
<div className="flex h-full items-center gap-2"> <div className="flex h-full items-center gap-2">
{isGlobalGroup && ( {isPublicGroup && (
<EarthIcon <EarthIcon
className="icon-md text-green-500" className="icon-md text-green-500"
aria-label={localize('com_ui_global_group')} aria-label={localize('com_ui_global_group')}

View file

@ -2,10 +2,9 @@ import { FileText, Plus } from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button, Skeleton } from '@librechat/client'; import { Button, Skeleton } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider'; import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { TPromptGroup, TStartupConfig } from 'librechat-data-provider'; import type { TPromptGroup } from 'librechat-data-provider';
import DashGroupItem from '~/components/Prompts/Groups/DashGroupItem'; import DashGroupItem from '~/components/Prompts/Groups/DashGroupItem';
import ChatGroupItem from '~/components/Prompts/Groups/ChatGroupItem'; import ChatGroupItem from '~/components/Prompts/Groups/ChatGroupItem';
import { useGetStartupConfig } from '~/data-provider';
import { useLocalize, useHasAccess } from '~/hooks'; import { useLocalize, useHasAccess } from '~/hooks';
import { cn } from '~/utils'; import { cn } from '~/utils';
@ -19,8 +18,6 @@ export default function List({
isLoading: boolean; isLoading: boolean;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();
const { data: startupConfig = {} as Partial<TStartupConfig> } = useGetStartupConfig();
const { instanceProjectId } = startupConfig;
const hasCreateAccess = useHasAccess({ const hasCreateAccess = useHasAccess({
permissionType: PermissionTypes.PROMPTS, permissionType: PermissionTypes.PROMPTS,
permission: Permissions.CREATE, permission: Permissions.CREATE,
@ -73,17 +70,9 @@ export default function List({
)} )}
{groups.map((group) => { {groups.map((group) => {
if (isChatRoute) { if (isChatRoute) {
return ( return <ChatGroupItem key={group._id} group={group} />;
<ChatGroupItem
key={group._id}
group={group}
instanceProjectId={instanceProjectId}
/>
);
} }
return ( return <DashGroupItem key={group._id} group={group} />;
<DashGroupItem key={group._id} group={group} instanceProjectId={instanceProjectId} />
);
})} })}
</div> </div>
</div> </div>

View file

@ -2,8 +2,8 @@
* @jest-environment jsdom * @jest-environment jsdom
*/ */
import * as React from 'react'; import * as React from 'react';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { render, waitFor, fireEvent } from '@testing-library/react'; import { render, waitFor, fireEvent } from '@testing-library/react';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { Agent } from 'librechat-data-provider'; import type { Agent } from 'librechat-data-provider';
@ -255,7 +255,6 @@ const mockAgentQuery = (
data: { data: {
id: 'agent-123', id: 'agent-123',
author: 'user-123', author: 'user-123',
isCollaborative: false,
...agent, ...agent,
} as Agent, } as Agent,
isInitialLoading: false, isInitialLoading: false,

View file

@ -26,8 +26,6 @@ mockUseWatch.mockImplementation(({ name }) => {
_id: 'agent-db-123', _id: 'agent-db-123',
name: 'Test Agent', name: 'Test Agent',
author: 'user-123', author: 'user-123',
projectIds: ['project-1'],
isCollaborative: false,
}; };
} }
if (name === 'id') { if (name === 'id') {
@ -237,8 +235,6 @@ describe('AgentFooter', () => {
_id: 'agent-db-123', _id: 'agent-db-123',
name: 'Test Agent', name: 'Test Agent',
author: 'user-123', author: 'user-123',
projectIds: ['project-1'],
isCollaborative: false,
}; };
} }
if (name === 'id') { if (name === 'id') {
@ -382,8 +378,6 @@ describe('AgentFooter', () => {
_id: 'agent-db-123', _id: 'agent-db-123',
name: 'Test Agent', name: 'Test Agent',
author: 'different-user', // Different author author: 'different-user', // Different author
projectIds: ['project-1'],
isCollaborative: false,
}; };
} }
if (name === 'id') { if (name === 'id') {
@ -409,8 +403,6 @@ describe('AgentFooter', () => {
_id: 'agent-db-123', _id: 'agent-db-123',
name: 'Test Agent', name: 'Test Agent',
author: 'user-123', // Same as current user author: 'user-123', // Same as current user
projectIds: ['project-1'],
isCollaborative: false,
}; };
} }
if (name === 'id') { if (name === 'id') {

View file

@ -46,12 +46,7 @@ export const useUpdatePromptGroup = (
]); ]);
const previousListData = groupListData ? structuredClone(groupListData) : undefined; const previousListData = groupListData ? structuredClone(groupListData) : undefined;
let update = variables.payload; const update = variables.payload;
if (update.removeProjectIds && group?.projectIds) {
update = structuredClone(update);
update.projectIds = group.projectIds.filter((id) => !update.removeProjectIds?.includes(id));
delete update.removeProjectIds;
}
if (groupListData) { if (groupListData) {
const newData = updateGroupFields( const newData = updateGroupFields(

View file

@ -2,16 +2,24 @@ const path = require('path');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
const { ensureRequiredCollectionsExist } = require('@librechat/api'); const { ensureRequiredCollectionsExist } = require('@librechat/api');
const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider'); const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider');
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
const connect = require('./connect'); const connect = require('./connect');
const { grantPermission } = require('~/server/services/PermissionService'); const { grantPermission } = require('~/server/services/PermissionService');
const { getProjectByName } = require('~/models/Project');
const { findRoleByIdentifier } = require('~/models'); const { findRoleByIdentifier } = require('~/models');
const { Agent, AclEntry } = require('~/db/models'); const { Agent, AclEntry } = require('~/db/models');
const GLOBAL_PROJECT_NAME = 'instance';
/** Queries the raw `projects` collection (which may still exist in the DB even though the model is removed) */
async function getGlobalProjectAgentIds(db) {
const project = await db
.collection('projects')
.findOne({ name: GLOBAL_PROJECT_NAME }, { projection: { agentIds: 1 } });
return new Set(project?.agentIds || []);
}
async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 } = {}) { async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100 } = {}) {
await connect(); await connect();
@ -24,7 +32,6 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
await ensureRequiredCollectionsExist(db); await ensureRequiredCollectionsExist(db);
} }
// Verify required roles exist
const ownerRole = await findRoleByIdentifier(AccessRoleIds.AGENT_OWNER); const ownerRole = await findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
const viewerRole = await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER); const viewerRole = await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
const editorRole = await findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR); const editorRole = await findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
@ -33,9 +40,7 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
throw new Error('Required roles not found. Run role seeding first.'); throw new Error('Required roles not found. Run role seeding first.');
} }
// Get global project agent IDs (stores agent.id, not agent._id) const globalAgentIds = db ? await getGlobalProjectAgentIds(db) : new Set();
const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']);
const globalAgentIds = new Set(globalProject?.agentIds || []);
logger.info(`Found ${globalAgentIds.size} agents in global project`); logger.info(`Found ${globalAgentIds.size} agents in global project`);
@ -52,9 +57,9 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
.lean(); .lean();
const categories = { const categories = {
globalEditAccess: [], // Global project + collaborative -> Public EDIT globalEditAccess: [],
globalViewAccess: [], // Global project + not collaborative -> Public VIEW globalViewAccess: [],
privateAgents: [], // Not in global project -> Private (owner only) privateAgents: [],
}; };
agentsToMigrate.forEach((agent) => { agentsToMigrate.forEach((agent) => {
@ -68,7 +73,6 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
} else { } else {
categories.privateAgents.push(agent); categories.privateAgents.push(agent);
// Log warning if private agent claims to be collaborative
if (isCollab) { if (isCollab) {
logger.warn( logger.warn(
`Agent "${agent.name}" (${agent.id}) has isCollaborative=true but is not in global project`, `Agent "${agent.name}" (${agent.id}) has isCollaborative=true but is not in global project`,
@ -130,7 +134,6 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
ownerGrants: 0, ownerGrants: 0,
}; };
// Process in batches
for (let i = 0; i < agentsToMigrate.length; i += batchSize) { for (let i = 0; i < agentsToMigrate.length; i += batchSize) {
const batch = agentsToMigrate.slice(i, i + batchSize); const batch = agentsToMigrate.slice(i, i + batchSize);
@ -143,7 +146,6 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
const isGlobal = globalAgentIds.has(agent.id); const isGlobal = globalAgentIds.has(agent.id);
const isCollab = agent.isCollaborative; const isCollab = agent.isCollaborative;
// Always grant owner permission to author
await grantPermission({ await grantPermission({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: agent.author, principalId: agent.author,
@ -154,24 +156,20 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
}); });
results.ownerGrants++; results.ownerGrants++;
// Determine public permissions for global project agents only
let publicRoleId = null; let publicRoleId = null;
let description = 'Private'; let description = 'Private';
if (isGlobal) { if (isGlobal) {
if (isCollab) { if (isCollab) {
// Global project + collaborative = Public EDIT access
publicRoleId = AccessRoleIds.AGENT_EDITOR; publicRoleId = AccessRoleIds.AGENT_EDITOR;
description = 'Global Edit'; description = 'Global Edit';
results.publicEditGrants++; results.publicEditGrants++;
} else { } else {
// Global project + not collaborative = Public VIEW access
publicRoleId = AccessRoleIds.AGENT_VIEWER; publicRoleId = AccessRoleIds.AGENT_VIEWER;
description = 'Global View'; description = 'Global View';
results.publicViewGrants++; results.publicViewGrants++;
} }
// Grant public permission
await grantPermission({ await grantPermission({
principalType: PrincipalType.PUBLIC, principalType: PrincipalType.PUBLIC,
principalId: null, principalId: null,
@ -200,7 +198,6 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
} }
} }
// Brief pause between batches
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
} }

View file

@ -2,16 +2,24 @@ const path = require('path');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
const { ensureRequiredCollectionsExist } = require('@librechat/api'); const { ensureRequiredCollectionsExist } = require('@librechat/api');
const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider'); const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider');
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
const connect = require('./connect'); const connect = require('./connect');
const { grantPermission } = require('~/server/services/PermissionService'); const { grantPermission } = require('~/server/services/PermissionService');
const { getProjectByName } = require('~/models/Project');
const { findRoleByIdentifier } = require('~/models'); const { findRoleByIdentifier } = require('~/models');
const { PromptGroup, AclEntry } = require('~/db/models'); const { PromptGroup, AclEntry } = require('~/db/models');
const GLOBAL_PROJECT_NAME = 'instance';
/** Queries the raw `projects` collection (which may still exist in the DB even though the model is removed) */
async function getGlobalProjectPromptGroupIds(db) {
const project = await db
.collection('projects')
.findOne({ name: GLOBAL_PROJECT_NAME }, { projection: { promptGroupIds: 1 } });
return new Set((project?.promptGroupIds || []).map((id) => id.toString()));
}
async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 } = {}) { async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100 } = {}) {
await connect(); await connect();
@ -24,7 +32,6 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
await ensureRequiredCollectionsExist(db); await ensureRequiredCollectionsExist(db);
} }
// Verify required roles exist
const ownerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER); const ownerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER);
const viewerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER); const viewerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER);
const editorRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR); const editorRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR);
@ -33,11 +40,7 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
throw new Error('Required promptGroup roles not found. Run role seeding first.'); throw new Error('Required promptGroup roles not found. Run role seeding first.');
} }
// Get global project prompt group IDs const globalPromptGroupIds = db ? await getGlobalProjectPromptGroupIds(db) : new Set();
const globalProject = await getProjectByName(GLOBAL_PROJECT_NAME, ['promptGroupIds']);
const globalPromptGroupIds = new Set(
(globalProject?.promptGroupIds || []).map((id) => id.toString()),
);
logger.info(`Found ${globalPromptGroupIds.size} prompt groups in global project`); logger.info(`Found ${globalPromptGroupIds.size} prompt groups in global project`);
@ -54,8 +57,8 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
.lean(); .lean();
const categories = { const categories = {
globalViewAccess: [], // PromptGroup in global project -> Public VIEW globalViewAccess: [],
privateGroups: [], // Not in global project -> Private (owner only) privateGroups: [],
}; };
promptGroupsToMigrate.forEach((group) => { promptGroupsToMigrate.forEach((group) => {
@ -115,7 +118,6 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
ownerGrants: 0, ownerGrants: 0,
}; };
// Process in batches
for (let i = 0; i < promptGroupsToMigrate.length; i += batchSize) { for (let i = 0; i < promptGroupsToMigrate.length; i += batchSize) {
const batch = promptGroupsToMigrate.slice(i, i + batchSize); const batch = promptGroupsToMigrate.slice(i, i + batchSize);
@ -127,7 +129,6 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
try { try {
const isGlobalGroup = globalPromptGroupIds.has(group._id.toString()); const isGlobalGroup = globalPromptGroupIds.has(group._id.toString());
// Always grant owner permission to author
await grantPermission({ await grantPermission({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: group.author, principalId: group.author,
@ -138,7 +139,6 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
}); });
results.ownerGrants++; results.ownerGrants++;
// Grant public view permissions for promptGroups in global project
if (isGlobalGroup) { if (isGlobalGroup) {
await grantPermission({ await grantPermission({
principalType: PrincipalType.PUBLIC, principalType: PrincipalType.PUBLIC,
@ -170,7 +170,6 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
} }
} }
// Brief pause between batches
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
} }

View file

@ -1,20 +1,13 @@
import { logger } from '@librechat/data-schemas'; import { logger } from '@librechat/data-schemas';
import { AccessRoleIds, ResourceType, PrincipalType, Constants } from 'librechat-data-provider'; import { AccessRoleIds, ResourceType, PrincipalType } from 'librechat-data-provider';
import { ensureRequiredCollectionsExist } from '../db/utils'; import { ensureRequiredCollectionsExist } from '../db/utils';
import type { AccessRoleMethods, IAgent } from '@librechat/data-schemas'; import type { AccessRoleMethods, IAgent } from '@librechat/data-schemas';
import type { Model, Mongoose } from 'mongoose'; import type { Model, Mongoose } from 'mongoose';
const { GLOBAL_PROJECT_NAME } = Constants; const GLOBAL_PROJECT_NAME = 'instance';
export interface MigrationCheckDbMethods { export interface MigrationCheckDbMethods {
findRoleByIdentifier: AccessRoleMethods['findRoleByIdentifier']; findRoleByIdentifier: AccessRoleMethods['findRoleByIdentifier'];
getProjectByName: (
projectName: string,
fieldsToSelect?: string[] | null,
) => Promise<{
agentIds?: string[];
[key: string]: unknown;
} | null>;
} }
export interface MigrationCheckParams { export interface MigrationCheckParams {
@ -60,7 +53,6 @@ export async function checkAgentPermissionsMigration({
await ensureRequiredCollectionsExist(db); await ensureRequiredCollectionsExist(db);
} }
// Verify required roles exist
const ownerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_OWNER); const ownerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
const viewerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER); const viewerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
const editorRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR); const editorRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
@ -77,9 +69,13 @@ export async function checkAgentPermissionsMigration({
}; };
} }
// Get global project agent IDs let globalAgentIds = new Set<string>();
const globalProject = await methods.getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']); if (db) {
const globalAgentIds = new Set(globalProject?.agentIds || []); const project = await db
.collection('projects')
.findOne({ name: GLOBAL_PROJECT_NAME }, { projection: { agentIds: 1 } });
globalAgentIds = new Set(project?.agentIds || []);
}
const AclEntry = mongoose.model('AclEntry'); const AclEntry = mongoose.model('AclEntry');
const migratedAgentIds = await AclEntry.distinct('resourceId', { const migratedAgentIds = await AclEntry.distinct('resourceId', {
@ -124,7 +120,6 @@ export async function checkAgentPermissionsMigration({
privateAgents: categories.privateAgents.length, privateAgents: categories.privateAgents.length,
}; };
// Add details for debugging
if (agentsToMigrate.length > 0) { if (agentsToMigrate.length > 0) {
result.details = { result.details = {
globalEditAccess: categories.globalEditAccess.map((a) => ({ globalEditAccess: categories.globalEditAccess.map((a) => ({
@ -152,7 +147,6 @@ export async function checkAgentPermissionsMigration({
return result; return result;
} catch (error) { } catch (error) {
logger.error('Failed to check agent permissions migration', error); logger.error('Failed to check agent permissions migration', error);
// Return zero counts on error to avoid blocking startup
return { return {
totalToMigrate: 0, totalToMigrate: 0,
globalEditAccess: 0, globalEditAccess: 0,
@ -170,7 +164,6 @@ export function logAgentMigrationWarning(result: MigrationCheckResult): void {
return; return;
} }
// Create a visible warning box
const border = '='.repeat(80); const border = '='.repeat(80);
const warning = [ const warning = [
'', '',
@ -201,10 +194,8 @@ export function logAgentMigrationWarning(result: MigrationCheckResult): void {
'', '',
]; ];
// Use console methods directly for visibility
console.log('\n' + warning.join('\n') + '\n'); console.log('\n' + warning.join('\n') + '\n');
// Also log with logger for consistency
logger.warn('Agent permissions migration required', { logger.warn('Agent permissions migration required', {
totalToMigrate: result.totalToMigrate, totalToMigrate: result.totalToMigrate,
globalEditAccess: result.globalEditAccess, globalEditAccess: result.globalEditAccess,

View file

@ -94,9 +94,6 @@ export const agentUpdateSchema = agentBaseSchema.extend({
avatar: z.union([agentAvatarSchema, z.null()]).optional(), avatar: z.union([agentAvatarSchema, z.null()]).optional(),
provider: z.string().optional(), provider: z.string().optional(),
model: z.string().nullable().optional(), model: z.string().nullable().optional(),
projectIds: z.array(z.string()).optional(),
removeProjectIds: z.array(z.string()).optional(),
isCollaborative: z.boolean().optional(),
}); });
interface ValidateAgentModelParams { interface ValidateAgentModelParams {

View file

@ -216,17 +216,12 @@ describe('access middleware', () => {
defaultParams.getRoleByName.mockResolvedValue(mockRole); defaultParams.getRoleByName.mockResolvedValue(mockRole);
const checkObject = { const checkObject = {};
projectIds: ['project1'],
removeProjectIds: ['project2'],
};
const result = await checkAccess({ const result = await checkAccess({
...defaultParams, ...defaultParams,
permissions: [Permissions.USE, Permissions.SHARE], permissions: [Permissions.USE, Permissions.SHARE],
bodyProps: { bodyProps: {} as Record<Permissions, string[]>,
[Permissions.SHARE]: ['projectIds', 'removeProjectIds'],
} as Record<Permissions, string[]>,
checkObject, checkObject,
}); });
expect(result).toBe(true); expect(result).toBe(true);
@ -244,17 +239,12 @@ describe('access middleware', () => {
defaultParams.getRoleByName.mockResolvedValue(mockRole); defaultParams.getRoleByName.mockResolvedValue(mockRole);
const checkObject = { const checkObject = {};
projectIds: ['project1'],
// missing removeProjectIds
};
const result = await checkAccess({ const result = await checkAccess({
...defaultParams, ...defaultParams,
permissions: [Permissions.SHARE], permissions: [Permissions.SHARE],
bodyProps: { bodyProps: {} as Record<Permissions, string[]>,
[Permissions.SHARE]: ['projectIds', 'removeProjectIds'],
} as Record<Permissions, string[]>,
checkObject, checkObject,
}); });
expect(result).toBe(false); expect(result).toBe(false);
@ -343,17 +333,12 @@ describe('access middleware', () => {
} as unknown as IRole; } as unknown as IRole;
mockGetRoleByName.mockResolvedValue(mockRole); mockGetRoleByName.mockResolvedValue(mockRole);
mockReq.body = { mockReq.body = {};
projectIds: ['project1'],
removeProjectIds: ['project2'],
};
const middleware = generateCheckAccess({ const middleware = generateCheckAccess({
permissionType: PermissionTypes.AGENTS, permissionType: PermissionTypes.AGENTS,
permissions: [Permissions.USE, Permissions.CREATE, Permissions.SHARE], permissions: [Permissions.USE, Permissions.CREATE, Permissions.SHARE],
bodyProps: { bodyProps: {} as Record<Permissions, string[]>,
[Permissions.SHARE]: ['projectIds', 'removeProjectIds'],
} as Record<Permissions, string[]>,
getRoleByName: mockGetRoleByName, getRoleByName: mockGetRoleByName,
}); });

View file

@ -1,20 +1,13 @@
import { logger } from '@librechat/data-schemas'; import { logger } from '@librechat/data-schemas';
import { AccessRoleIds, ResourceType, PrincipalType, Constants } from 'librechat-data-provider'; import { AccessRoleIds, ResourceType, PrincipalType } from 'librechat-data-provider';
import { ensureRequiredCollectionsExist } from '../db/utils'; import { ensureRequiredCollectionsExist } from '../db/utils';
import type { AccessRoleMethods, IPromptGroupDocument } from '@librechat/data-schemas'; import type { AccessRoleMethods, IPromptGroupDocument } from '@librechat/data-schemas';
import type { Model, Mongoose } from 'mongoose'; import type { Model, Mongoose } from 'mongoose';
const { GLOBAL_PROJECT_NAME } = Constants; const GLOBAL_PROJECT_NAME = 'instance';
export interface PromptMigrationCheckDbMethods { export interface PromptMigrationCheckDbMethods {
findRoleByIdentifier: AccessRoleMethods['findRoleByIdentifier']; findRoleByIdentifier: AccessRoleMethods['findRoleByIdentifier'];
getProjectByName: (
projectName: string,
fieldsToSelect?: string[] | null,
) => Promise<{
promptGroupIds?: string[];
[key: string]: unknown;
} | null>;
} }
export interface PromptMigrationCheckParams { export interface PromptMigrationCheckParams {
@ -53,13 +46,11 @@ export async function checkPromptPermissionsMigration({
logger.debug('Checking if prompt permissions migration is needed'); logger.debug('Checking if prompt permissions migration is needed');
try { try {
/** Native MongoDB database instance */
const db = mongoose.connection.db; const db = mongoose.connection.db;
if (db) { if (db) {
await ensureRequiredCollectionsExist(db); await ensureRequiredCollectionsExist(db);
} }
// Verify required roles exist
const ownerRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER); const ownerRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER);
const viewerRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER); const viewerRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER);
const editorRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR); const editorRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR);
@ -75,11 +66,15 @@ export async function checkPromptPermissionsMigration({
}; };
} }
/** Global project prompt group IDs */ let globalPromptGroupIds = new Set<string>();
const globalProject = await methods.getProjectByName(GLOBAL_PROJECT_NAME, ['promptGroupIds']); if (db) {
const globalPromptGroupIds = new Set( const project = await db
(globalProject?.promptGroupIds || []).map((id) => id.toString()), .collection('projects')
); .findOne({ name: GLOBAL_PROJECT_NAME }, { projection: { promptGroupIds: 1 } });
globalPromptGroupIds = new Set(
(project?.promptGroupIds || []).map((id: { toString(): string }) => id.toString()),
);
}
const AclEntry = mongoose.model('AclEntry'); const AclEntry = mongoose.model('AclEntry');
const migratedGroupIds = await AclEntry.distinct('resourceId', { const migratedGroupIds = await AclEntry.distinct('resourceId', {
@ -118,7 +113,6 @@ export async function checkPromptPermissionsMigration({
privateGroups: categories.privateGroups.length, privateGroups: categories.privateGroups.length,
}; };
// Add details for debugging
if (promptGroupsToMigrate.length > 0) { if (promptGroupsToMigrate.length > 0) {
result.details = { result.details = {
globalViewAccess: categories.globalViewAccess.map((g) => ({ globalViewAccess: categories.globalViewAccess.map((g) => ({
@ -143,7 +137,6 @@ export async function checkPromptPermissionsMigration({
return result; return result;
} catch (error) { } catch (error) {
logger.error('Failed to check prompt permissions migration', error); logger.error('Failed to check prompt permissions migration', error);
// Return zero counts on error to avoid blocking startup
return { return {
totalToMigrate: 0, totalToMigrate: 0,
globalViewAccess: 0, globalViewAccess: 0,
@ -160,7 +153,6 @@ export function logPromptMigrationWarning(result: PromptMigrationCheckResult): v
return; return;
} }
// Create a visible warning box
const border = '='.repeat(80); const border = '='.repeat(80);
const warning = [ const warning = [
'', '',
@ -190,10 +182,8 @@ export function logPromptMigrationWarning(result: PromptMigrationCheckResult): v
'', '',
]; ];
// Use console methods directly for visibility
console.log('\n' + warning.join('\n') + '\n'); console.log('\n' + warning.join('\n') + '\n');
// Also log with logger for consistency
logger.warn('Prompt permissions migration required', { logger.warn('Prompt permissions migration required', {
totalToMigrate: result.totalToMigrate, totalToMigrate: result.totalToMigrate,
globalViewAccess: result.globalViewAccess, globalViewAccess: result.globalViewAccess,

View file

@ -30,26 +30,6 @@ describe('updatePromptGroupSchema', () => {
} }
}); });
it('should accept valid projectIds array', () => {
const result = updatePromptGroupSchema.safeParse({
projectIds: ['proj1', 'proj2'],
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.projectIds).toEqual(['proj1', 'proj2']);
}
});
it('should accept valid removeProjectIds array', () => {
const result = updatePromptGroupSchema.safeParse({
removeProjectIds: ['proj1'],
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.removeProjectIds).toEqual(['proj1']);
}
});
it('should accept valid command field', () => { it('should accept valid command field', () => {
const result = updatePromptGroupSchema.safeParse({ command: 'my-command-123' }); const result = updatePromptGroupSchema.safeParse({ command: 'my-command-123' });
expect(result.success).toBe(true); expect(result.success).toBe(true);

View file

@ -14,10 +14,6 @@ export const updatePromptGroupSchema = z
oneliner: z.string().max(500).optional(), oneliner: z.string().max(500).optional(),
/** Category for organizing prompt groups */ /** Category for organizing prompt groups */
category: z.string().max(100).optional(), category: z.string().max(100).optional(),
/** Project IDs to add for sharing */
projectIds: z.array(z.string()).optional(),
/** Project IDs to remove from sharing */
removeProjectIds: z.array(z.string()).optional(),
/** Command shortcut for the prompt group */ /** Command shortcut for the prompt group */
command: z command: z
.string() .string()

View file

@ -781,7 +781,6 @@ export type TStartupConfig = {
sharedLinksEnabled: boolean; sharedLinksEnabled: boolean;
publicSharedLinksEnabled: boolean; publicSharedLinksEnabled: boolean;
analyticsGtmId?: string; analyticsGtmId?: string;
instanceProjectId: string;
bundlerURL?: string; bundlerURL?: string;
staticBundlerURL?: string; staticBundlerURL?: string;
sharePointFilePickerEnabled?: boolean; sharePointFilePickerEnabled?: boolean;
@ -1758,8 +1757,6 @@ export enum Constants {
SAVED_TAG = 'Saved', SAVED_TAG = 'Saved',
/** Max number of Conversation starters for Agents/Assistants */ /** Max number of Conversation starters for Agents/Assistants */
MAX_CONVO_STARTERS = 4, MAX_CONVO_STARTERS = 4,
/** Global/instance Project Name */
GLOBAL_PROJECT_NAME = 'instance',
/** Delimiter for MCP tools */ /** Delimiter for MCP tools */
mcp_delimiter = '_mcp_', mcp_delimiter = '_mcp_',
/** Prefix for MCP plugins */ /** Prefix for MCP plugins */

View file

@ -258,11 +258,8 @@ export const defaultAgentFormValues = {
tools: [], tools: [],
tool_options: {}, tool_options: {},
provider: {}, provider: {},
projectIds: [],
edges: [], edges: [],
artifacts: '', artifacts: '',
/** @deprecated Use ACL permissions instead */
isCollaborative: false,
recursion_limit: undefined, recursion_limit: undefined,
[Tools.execute_code]: false, [Tools.execute_code]: false,
[Tools.file_search]: false, [Tools.file_search]: false,

View file

@ -541,7 +541,6 @@ export type TPromptGroup = {
command?: string; command?: string;
oneliner?: string; oneliner?: string;
category?: string; category?: string;
projectIds?: string[];
productionId?: string | null; productionId?: string | null;
productionPrompt?: Pick<TPrompt, 'prompt'> | null; productionPrompt?: Pick<TPrompt, 'prompt'> | null;
author: string; author: string;
@ -594,9 +593,7 @@ export type TCreatePromptResponse = {
group?: TPromptGroup; group?: TPromptGroup;
}; };
export type TUpdatePromptGroupPayload = Partial<TPromptGroup> & { export type TUpdatePromptGroupPayload = Partial<TPromptGroup>;
removeProjectIds?: string[];
};
export type TUpdatePromptGroupVariables = { export type TUpdatePromptGroupVariables = {
id: string; id: string;

View file

@ -252,15 +252,12 @@ export type Agent = {
instructions?: string | null; instructions?: string | null;
additional_instructions?: string | null; additional_instructions?: string | null;
tools?: string[]; tools?: string[];
projectIds?: string[];
tool_kwargs?: Record<string, unknown>; tool_kwargs?: Record<string, unknown>;
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
provider: AgentProvider; provider: AgentProvider;
model: string | null; model: string | null;
model_parameters: AgentModelParameters; model_parameters: AgentModelParameters;
conversation_starters?: string[]; conversation_starters?: string[];
/** @deprecated Use ACL permissions instead */
isCollaborative?: boolean;
tool_resources?: AgentToolResources; tool_resources?: AgentToolResources;
/** @deprecated Use edges instead */ /** @deprecated Use edges instead */
agent_ids?: string[]; agent_ids?: string[];
@ -313,9 +310,6 @@ export type AgentUpdateParams = {
provider?: AgentProvider; provider?: AgentProvider;
model?: string | null; model?: string | null;
model_parameters?: AgentModelParameters; model_parameters?: AgentModelParameters;
projectIds?: string[];
removeProjectIds?: string[];
isCollaborative?: boolean;
} & Pick< } & Pick<
Agent, Agent,
| 'agent_ids' | 'agent_ids'

View file

@ -24,7 +24,6 @@ const agentSchema = new Schema({
id: { type: String, required: true }, id: { type: String, required: true },
name: { type: String, required: true }, name: { type: String, required: true },
author: { type: String }, author: { type: String },
isCollaborative: { type: Boolean, default: false },
}); });
const promptGroupSchema = new Schema({ const promptGroupSchema = new Schema({
@ -107,7 +106,7 @@ describeIfFerretDB('Migration anti-join → $nin - FerretDB compatibility', () =
_id: { $nin: migratedIds }, _id: { $nin: migratedIds },
author: { $exists: true, $ne: null }, author: { $exists: true, $ne: null },
}) })
.select('_id id name author isCollaborative') .select('_id id name author')
.lean(); .lean();
expect(toMigrate).toHaveLength(2); expect(toMigrate).toHaveLength(2);
@ -197,7 +196,6 @@ describeIfFerretDB('Migration anti-join → $nin - FerretDB compatibility', () =
id: 'proj_agent', id: 'proj_agent',
name: 'Field Test', name: 'Field Test',
author: 'user1', author: 'user1',
isCollaborative: true,
}); });
const migratedIds = await AclEntry.distinct('resourceId', { const migratedIds = await AclEntry.distinct('resourceId', {
@ -209,7 +207,7 @@ describeIfFerretDB('Migration anti-join → $nin - FerretDB compatibility', () =
_id: { $nin: migratedIds }, _id: { $nin: migratedIds },
author: { $exists: true, $ne: null }, author: { $exists: true, $ne: null },
}) })
.select('_id id name author isCollaborative') .select('_id id name author')
.lean(); .lean();
expect(toMigrate).toHaveLength(1); expect(toMigrate).toHaveLength(1);
@ -218,7 +216,6 @@ describeIfFerretDB('Migration anti-join → $nin - FerretDB compatibility', () =
expect(agent).toHaveProperty('id', 'proj_agent'); expect(agent).toHaveProperty('id', 'proj_agent');
expect(agent).toHaveProperty('name', 'Field Test'); expect(agent).toHaveProperty('name', 'Field Test');
expect(agent).toHaveProperty('author', 'user1'); expect(agent).toHaveProperty('author', 'user1');
expect(agent).toHaveProperty('isCollaborative', true);
}); });
}); });

View file

@ -27,7 +27,6 @@ const promptGroupSchema = new Schema(
author: { type: Schema.Types.ObjectId, required: true, index: true }, author: { type: Schema.Types.ObjectId, required: true, index: true },
authorName: { type: String, required: true }, authorName: { type: String, required: true },
command: { type: String }, command: { type: String },
projectIds: { type: [Schema.Types.ObjectId], default: [] },
}, },
{ timestamps: true }, { timestamps: true },
); );
@ -51,7 +50,6 @@ type PromptGroupDoc = mongoose.Document & {
oneliner: string; oneliner: string;
numberOfGenerations: number; numberOfGenerations: number;
command?: string; command?: string;
projectIds: Types.ObjectId[];
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
}; };
@ -226,7 +224,7 @@ describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () =>
.skip(skip) .skip(skip)
.limit(limit) .limit(limit)
.select( .select(
'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt',
) )
.lean(), .lean(),
PromptGroup.countDocuments(query), PromptGroup.countDocuments(query),
@ -273,7 +271,7 @@ describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () =>
.sort({ updatedAt: -1, _id: 1 }) .sort({ updatedAt: -1, _id: 1 })
.limit(normalizedLimit + 1) .limit(normalizedLimit + 1)
.select( .select(
'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt',
) )
.lean(); .lean();
@ -303,7 +301,7 @@ describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () =>
const groups = await PromptGroup.find({ _id: { $in: accessibleIds } }) const groups = await PromptGroup.find({ _id: { $in: accessibleIds } })
.sort({ updatedAt: -1, _id: 1 }) .sort({ updatedAt: -1, _id: 1 })
.select( .select(
'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt',
) )
.lean(); .lean();
@ -326,7 +324,7 @@ describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () =>
const groups = await PromptGroup.find({}) const groups = await PromptGroup.find({})
.select( .select(
'name numberOfGenerations oneliner category projectIds productionId author authorName createdAt updatedAt', 'name numberOfGenerations oneliner category productionId author authorName createdAt updatedAt',
) )
.lean(); .lean();
const result = await attachProductionPrompts( const result = await attachProductionPrompts(
@ -339,7 +337,6 @@ describeIfFerretDB('Prompt $lookup replacement - FerretDB compatibility', () =>
expect(item.numberOfGenerations).toBe(5); expect(item.numberOfGenerations).toBe(5);
expect(item.oneliner).toBe('A test prompt'); expect(item.oneliner).toBe('A test prompt');
expect(item.category).toBe('testing'); expect(item.category).toBe('testing');
expect(item.projectIds).toEqual([]);
expect(item.productionId).toBeDefined(); expect(item.productionId).toBeDefined();
expect(item.author).toBeDefined(); expect(item.author).toBeDefined();
expect(item.authorName).toBe('Test User'); expect(item.authorName).toBe('Test User');

View file

@ -37,7 +37,6 @@ const projectSchema = new Schema({
const agentSchema = new Schema({ const agentSchema = new Schema({
name: { type: String, required: true }, name: { type: String, required: true },
projectIds: { type: [String], default: [] },
tool_resources: { type: Schema.Types.Mixed, default: {} }, tool_resources: { type: Schema.Types.Mixed, default: {} },
}); });
@ -197,23 +196,6 @@ describeIfFerretDB('$pullAll FerretDB compatibility', () => {
expect(doc.agentIds).toEqual(['a2', 'a4']); expect(doc.agentIds).toEqual(['a2', 'a4']);
}); });
it('should remove projectIds from an agent', async () => {
await Agent.create({
name: 'Test Agent',
projectIds: ['p1', 'p2', 'p3'],
});
await Agent.findOneAndUpdate(
{ name: 'Test Agent' },
{ $pullAll: { projectIds: ['p1', 'p3'] } },
{ new: true },
);
const updated = await Agent.findOne({ name: 'Test Agent' }).lean();
const doc = updated as Record<string, unknown>;
expect(doc.projectIds).toEqual(['p2']);
});
it('should handle removing from nested dynamic paths (tool_resources)', async () => { it('should handle removing from nested dynamic paths (tool_resources)', async () => {
await Agent.create({ await Agent.create({
name: 'Resource Agent', name: 'Resource Agent',

View file

@ -13,7 +13,6 @@ import { createActionModel } from './action';
import { createAssistantModel } from './assistant'; import { createAssistantModel } from './assistant';
import { createFileModel } from './file'; import { createFileModel } from './file';
import { createBannerModel } from './banner'; import { createBannerModel } from './banner';
import { createProjectModel } from './project';
import { createKeyModel } from './key'; import { createKeyModel } from './key';
import { createPluginAuthModel } from './pluginAuth'; import { createPluginAuthModel } from './pluginAuth';
import { createTransactionModel } from './transaction'; import { createTransactionModel } from './transaction';
@ -48,7 +47,6 @@ export function createModels(mongoose: typeof import('mongoose')) {
Assistant: createAssistantModel(mongoose), Assistant: createAssistantModel(mongoose),
File: createFileModel(mongoose), File: createFileModel(mongoose),
Banner: createBannerModel(mongoose), Banner: createBannerModel(mongoose),
Project: createProjectModel(mongoose),
Key: createKeyModel(mongoose), Key: createKeyModel(mongoose),
PluginAuth: createPluginAuthModel(mongoose), PluginAuth: createPluginAuthModel(mongoose),
Transaction: createTransactionModel(mongoose), Transaction: createTransactionModel(mongoose),

View file

@ -1,8 +0,0 @@
import projectSchema, { IMongoProject } from '~/schema/project';
/**
* Creates or returns the Project model using the provided mongoose instance and schema
*/
export function createProjectModel(mongoose: typeof import('mongoose')) {
return mongoose.models.Project || mongoose.model<IMongoProject>('Project', projectSchema);
}

View file

@ -76,10 +76,6 @@ const agentSchema = new Schema<IAgent>(
type: [{ type: Schema.Types.Mixed }], type: [{ type: Schema.Types.Mixed }],
default: [], default: [],
}, },
isCollaborative: {
type: Boolean,
default: undefined,
},
conversation_starters: { conversation_starters: {
type: [String], type: [String],
default: [], default: [],
@ -88,11 +84,6 @@ const agentSchema = new Schema<IAgent>(
type: Schema.Types.Mixed, type: Schema.Types.Mixed,
default: {}, default: {},
}, },
projectIds: {
type: [Schema.Types.ObjectId],
ref: 'Project',
index: true,
},
versions: { versions: {
type: [Schema.Types.Mixed], type: [Schema.Types.Mixed],
default: [], default: [],

View file

@ -13,7 +13,6 @@ export { default as keySchema } from './key';
export { default as messageSchema } from './message'; export { default as messageSchema } from './message';
export { default as pluginAuthSchema } from './pluginAuth'; export { default as pluginAuthSchema } from './pluginAuth';
export { default as presetSchema } from './preset'; export { default as presetSchema } from './preset';
export { default as projectSchema } from './project';
export { default as promptSchema } from './prompt'; export { default as promptSchema } from './prompt';
export { default as promptGroupSchema } from './promptGroup'; export { default as promptGroupSchema } from './promptGroup';
export { default as roleSchema } from './role'; export { default as roleSchema } from './role';

View file

@ -1,34 +0,0 @@
import { Schema, Document, Types } from 'mongoose';
export interface IMongoProject extends Document {
name: string;
promptGroupIds: Types.ObjectId[];
agentIds: string[];
createdAt?: Date;
updatedAt?: Date;
}
const projectSchema = new Schema<IMongoProject>(
{
name: {
type: String,
required: true,
index: true,
},
promptGroupIds: {
type: [Schema.Types.ObjectId],
ref: 'PromptGroup',
default: [],
},
agentIds: {
type: [String],
ref: 'Agent',
default: [],
},
},
{
timestamps: true,
},
);
export default projectSchema;

View file

@ -22,12 +22,6 @@ const promptGroupSchema = new Schema<IPromptGroupDocument>(
default: '', default: '',
index: true, index: true,
}, },
projectIds: {
type: [Schema.Types.ObjectId],
ref: 'Project',
index: true,
default: [],
},
productionId: { productionId: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Prompt', ref: 'Prompt',

View file

@ -31,11 +31,8 @@ export interface IAgent extends Omit<Document, 'model'> {
/** @deprecated Use edges instead */ /** @deprecated Use edges instead */
agent_ids?: string[]; agent_ids?: string[];
edges?: GraphEdge[]; edges?: GraphEdge[];
/** @deprecated Use ACL permissions instead */
isCollaborative?: boolean;
conversation_starters?: string[]; conversation_starters?: string[];
tool_resources?: unknown; tool_resources?: unknown;
projectIds?: Types.ObjectId[];
versions?: Omit<IAgent, 'versions'>[]; versions?: Omit<IAgent, 'versions'>[];
category: string; category: string;
support_contact?: ISupportContact; support_contact?: ISupportContact;

View file

@ -14,7 +14,6 @@ export interface IPromptGroup {
numberOfGenerations: number; numberOfGenerations: number;
oneliner: string; oneliner: string;
category: string; category: string;
projectIds: Types.ObjectId[];
productionId: Types.ObjectId; productionId: Types.ObjectId;
author: Types.ObjectId; author: Types.ObjectId;
authorName: string; authorName: string;