mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-08 09:02:36 +01:00
🗑️ 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:
parent
0efa41a518
commit
30df7b2ba5
41 changed files with 94 additions and 821 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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] },
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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, {
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 || '');
|
||||||
|
|
|
||||||
|
|
@ -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')}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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') {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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: [],
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue