mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 01:10:14 +01:00
🏪 feat: Agent Marketplace
bugfix: Enhance Agent and AgentCategory schemas with new fields for category, support contact, and promotion status refactored and moved agent category methods and schema to data-schema package 🔧 fix: Merge and Rebase Conflicts - Move AgentCategory from api/models to @packages/data-schemas structure - Add schema, types, methods, and model following codebase conventions - Implement auto-seeding of default categories during AppService startup - Update marketplace controller to use new data-schemas methods - Remove old model file and standalone seed script refactor: unify agent marketplace to single endpoint with cursor pagination - Replace multiple marketplace routes with unified /marketplace endpoint - Add query string controls: category, search, limit, cursor, promoted, requiredPermission - Implement cursor-based pagination replacing page-based system - Integrate ACL permissions for proper access control - Fix ObjectId constructor error in Agent model - Update React components to use unified useGetMarketplaceAgentsQuery hook - Enhance type safety and remove deprecated useDynamicAgentQuery - Update tests for new marketplace architecture -Known issues: see more button after category switching + Unit tests feat: add icon property to ProcessedAgentCategory interface - Add useMarketplaceAgentsInfiniteQuery and useGetAgentCategoriesQuery to client/src/data-provider/Agents/ - Replace manual pagination in AgentGrid with infinite query pattern - Update imports to use local data provider instead of librechat-data-provider - Add proper permission handling with PERMISSION_BITS.VIEW/EDIT constants - Improve agent access control by adding requiredPermission validation in backend - Remove manual cursor/state management in favor of infinite query built-ins - Maintain existing search and category filtering functionality refactor: consolidate agent marketplace endpoints into main agents API and improve data management consistency - Remove dedicated marketplace controller and routes, merging functionality into main agents v1 API - Add countPromotedAgents function to Agent model for promoted agents count - Enhance getListAgents handler with marketplace filtering (category, search, promoted status) - Move getAgentCategories from marketplace to v1 controller with same functionality - Update agent mutations to invalidate marketplace queries and handle multiple permission levels - Improve cache management by updating all agent query variants (VIEW/EDIT permissions) - Consolidate agent data access patterns for better maintainability and consistency - Remove duplicate marketplace route definitions and middleware selected view only agents injected in the drop down fix: remove minlength validation for support contact name in agent schema feat: add validation and error messages for agent name in AgentConfig and AgentPanel fix: update agent permission check logic in AgentPanel to simplify condition Fix linting WIP Fix Unit tests WIP ESLint fixes eslint fix refactor: enhance isDuplicateVersion function in Agent model for improved comparison logic - Introduced handling for undefined/null values in array and object comparisons. - Normalized array comparisons to treat undefined/null as empty arrays. - Added deep comparison for objects and improved handling of primitive values. - Enhanced projectIds comparison to ensure consistent MongoDB ObjectId handling. refactor: remove redundant properties from IAgent interface in agent schema chore: update localization for agent detail component and clean up imports ci: update access middleware tests chore: remove unused PermissionTypes import from Role model ci: update AclEntry model tests ci: update button accessibility labels in AgentDetail tests refactor: update exhaustive dep. lint warning 🔧 fix: Fixed agent actions access feat: Add role-level permissions for agent sharing people picker - Add PEOPLE_PICKER permission type with VIEW_USERS and VIEW_GROUPS permissions - Create custom middleware for query-aware permission validation - Implement permission-based type filtering in PeoplePicker component - Hide people picker UI when user lacks permissions, show only public toggle - Support granular access: users-only, groups-only, or mixed search modes refactor: Replace marketplace interface config with permission-based system - Add MARKETPLACE permission type to handle marketplace access control - Update interface configuration to use role-based marketplace settings (admin/user) - Replace direct marketplace boolean config with permission-based checks - Modify frontend components to use marketplace permissions instead of interface config - Update agent query hooks to use marketplace permissions for determining permission levels - Add marketplace configuration structure similar to peoplePicker in YAML config - Backend now sets MARKETPLACE permissions based on interface configuration - When marketplace enabled: users get agents with EDIT permissions in dropdown lists (builder mode) - When marketplace disabled: users get agents with VIEW permissions in dropdown lists (browse mode) 🔧 fix: Redirect to New Chat if No Marketplace Access and Required Agent Name Placeholder (#8213) * Fix: Fix the redirect to new chat page if access to marketplace is denied * Fixed the required agent name placeholder --------- Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com> chore: fix tests, remove unnecessary imports refactor: Implement permission checks for file access via agents - Updated `hasAccessToFilesViaAgent` to utilize permission checks for VIEW and EDIT access. - Replaced project-based access validation with permission-based checks. - Enhanced tests to cover new permission logic and ensure proper access control for files associated with agents. - Cleaned up imports and initialized models in test files for consistency. refactor: Enhance test setup and cleanup for file access control - Introduced modelsToCleanup array to track models added during tests for proper cleanup. - Updated afterAll hooks in test files to ensure all collections are cleared and only added models are deleted. - Improved consistency in model initialization across test files. - Added comments for clarity on cleanup processes and test data management. chore: Update Jest configuration and test setup for improved timeout handling - Added a global test timeout of 30 seconds in jest.config.js. - Configured jest.setTimeout in jestSetup.js to allow individual test overrides if needed. - Enhanced test reliability by ensuring consistent timeout settings across all tests. refactor: Implement file access filtering based on agent permissions - Introduced `filterFilesByAgentAccess` function to filter files based on user access through agents. - Updated `getFiles` and `primeFiles` functions to utilize the new filtering logic. - Moved `hasAccessToFilesViaAgent` function from the File model to permission services, adjusting imports accordingly - Enhanced tests to ensure proper access control and filtering behavior for files associated with agents. fix: make support_contact field a nested object rather than a sub-document refactor: Update support_contact field initialization in agent model - Removed handling for empty support_contact object in createAgent function. - Changed default value of support_contact in agent schema to undefined. test: Add comprehensive tests for support_contact field handling and versioning refactor: remove unused avatar upload mutation field and add informational toast for success chore: add missing SidePanelProvider for AgentMarketplace and organize imports fix: resolve agent selection race condition in marketplace HandleStartChat - Set agent in localStorage before newConversation to prevent useSelectorEffects from auto-selecting previous agent fix: resolve agent dropdown showing raw ID instead of agent info from URL - Add proactive agent fetching when agent_id is present in URL parameters - Inject fetched agent into agents cache so dropdowns display proper name/avatar - Use useAgentsMap dependency to ensure proper cache initialization timing - Prevents raw agent IDs from showing in UI when visiting shared agent links Fix: Agents endpoint renamed to "My Agent" for less confusion with the Marketplace agents. chore: fix ESLint issues and Test Mocks ci: update permissions structure in loadDefaultInterface tests - Refactored permissions for MEMORY and added new permissions for MARKETPLACE and PEOPLE_PICKER. - Ensured consistent structure for permissions across different types. feat: support_contact validation to allow empty email strings
This commit is contained in:
parent
66bd419baa
commit
949682ef0f
95 changed files with 3770 additions and 2728 deletions
|
|
@ -1,255 +0,0 @@
|
|||
const AgentCategory = require('~/models/AgentCategory');
|
||||
const mongoose = require('mongoose');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
// Get the Agent model
|
||||
const Agent = mongoose.model('agent');
|
||||
|
||||
// Default page size for agent browsing
|
||||
const DEFAULT_PAGE_SIZE = 6;
|
||||
|
||||
/**
|
||||
* Common pagination utility for agent queries
|
||||
*
|
||||
* @param {Object} filter - MongoDB filter object
|
||||
* @param {number} page - Page number (1-based)
|
||||
* @param {number} limit - Items per page
|
||||
* @returns {Promise<Object>} Paginated results with agents and pagination info
|
||||
*/
|
||||
const paginateAgents = async (filter, page = 1, limit = DEFAULT_PAGE_SIZE) => {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Get total count for pagination
|
||||
const total = await Agent.countDocuments(filter);
|
||||
|
||||
// Get agents with pagination
|
||||
const agents = await Agent.find(filter)
|
||||
.select('id name description avatar category support_contact authorName')
|
||||
.sort({ updatedAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
// Calculate if there are more agents to load
|
||||
const hasMore = total > page * limit;
|
||||
|
||||
return {
|
||||
agents,
|
||||
pagination: {
|
||||
current: page,
|
||||
hasMore,
|
||||
total,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get promoted/top picks agents with pagination
|
||||
* Can also return all agents when showAll=true parameter is provided
|
||||
*
|
||||
* @param {Object} req - Express request object
|
||||
* @param {Object} res - Express response object
|
||||
*/
|
||||
const getPromotedAgents = async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || DEFAULT_PAGE_SIZE;
|
||||
|
||||
// Check if this is a request for "all" agents via query parameter
|
||||
const showAllAgents = req.query.showAll === 'true';
|
||||
|
||||
// Base filter for shared agents only
|
||||
const filter = {
|
||||
projectIds: { $exists: true, $ne: [] }, // Only get shared agents
|
||||
};
|
||||
|
||||
// Only add promoted filter if not requesting all agents
|
||||
if (!showAllAgents) {
|
||||
filter.is_promoted = true; // Only get promoted agents
|
||||
}
|
||||
|
||||
const result = await paginateAgents(filter, page, limit);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
logger.error('[/Agents/Marketplace] Error fetching promoted agents:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch promoted agents',
|
||||
userMessage: 'Unable to load agents. Please try refreshing the page.',
|
||||
suggestion: 'Try refreshing the page or check your network connection',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get agents by category with pagination
|
||||
*
|
||||
* @param {Object} req - Express request object
|
||||
* @param {Object} res - Express response object
|
||||
*/
|
||||
const getAgentsByCategory = async (req, res) => {
|
||||
try {
|
||||
const { category } = req.params;
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || DEFAULT_PAGE_SIZE;
|
||||
|
||||
const filter = {
|
||||
category,
|
||||
projectIds: { $exists: true, $ne: [] }, // Only get shared agents
|
||||
};
|
||||
|
||||
const result = await paginateAgents(filter, page, limit);
|
||||
|
||||
// Get category description from database
|
||||
const categoryDoc = await AgentCategory.findOne({ value: category, isActive: true });
|
||||
const categoryInfo = {
|
||||
name: category,
|
||||
description: categoryDoc?.description || '',
|
||||
total: result.pagination.total,
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
...result,
|
||||
category: categoryInfo,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[/Agents/Marketplace] Error fetching agents for category ${req.params.category}:`,
|
||||
error,
|
||||
);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch agents by category',
|
||||
userMessage: `Unable to load agents for this category. Please try a different category.`,
|
||||
suggestion: 'Try selecting a different category or refresh the page',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Search agents with filters
|
||||
*
|
||||
* @param {Object} req - Express request object
|
||||
* @param {Object} res - Express response object
|
||||
*/
|
||||
const searchAgents = async (req, res) => {
|
||||
try {
|
||||
const { q, category } = req.query;
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || DEFAULT_PAGE_SIZE;
|
||||
|
||||
if (!q || q.trim() === '') {
|
||||
return res.status(400).json({
|
||||
error: 'Search query is required',
|
||||
userMessage: 'Please enter a search term to find agents',
|
||||
suggestion: 'Enter a search term to find agents by name or description',
|
||||
});
|
||||
}
|
||||
|
||||
// Build search filter
|
||||
const filter = {
|
||||
projectIds: { $exists: true, $ne: [] }, // Only get shared agents
|
||||
$or: [
|
||||
{ name: { $regex: q, $options: 'i' } }, // Case-insensitive name search
|
||||
{ description: { $regex: q, $options: 'i' } },
|
||||
],
|
||||
};
|
||||
|
||||
// Add category filter if provided
|
||||
if (category && category !== 'all') {
|
||||
filter.category = category;
|
||||
}
|
||||
|
||||
const result = await paginateAgents(filter, page, limit);
|
||||
|
||||
res.status(200).json({
|
||||
...result,
|
||||
query: q,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[/Agents/Marketplace] Error searching agents:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to search agents',
|
||||
userMessage: 'Search is temporarily unavailable. Please try again.',
|
||||
suggestion: 'Try a different search term or check your network connection',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all agent categories with counts
|
||||
*
|
||||
* @param {Object} _req - Express request object (unused)
|
||||
* @param {Object} res - Express response object
|
||||
*/
|
||||
const getAgentCategories = async (_req, res) => {
|
||||
try {
|
||||
// Get categories with agent counts from database
|
||||
const categories = await AgentCategory.getCategoriesWithCounts();
|
||||
|
||||
// Get count of promoted agents for Top Picks
|
||||
const promotedCount = await Agent.countDocuments({
|
||||
projectIds: { $exists: true, $ne: [] },
|
||||
is_promoted: true,
|
||||
});
|
||||
|
||||
// Convert to marketplace format (TCategory structure)
|
||||
const formattedCategories = categories.map((category) => ({
|
||||
value: category.value,
|
||||
label: category.label,
|
||||
count: category.agentCount,
|
||||
description: category.description,
|
||||
}));
|
||||
|
||||
// Add promoted category if agents exist
|
||||
if (promotedCount > 0) {
|
||||
formattedCategories.unshift({
|
||||
value: 'promoted',
|
||||
label: 'Promoted',
|
||||
count: promotedCount,
|
||||
description: 'Our recommended agents',
|
||||
});
|
||||
}
|
||||
|
||||
// Get total count of all shared agents for "All" category
|
||||
const totalAgents = await Agent.countDocuments({
|
||||
projectIds: { $exists: true, $ne: [] },
|
||||
});
|
||||
|
||||
// Add "All" category at the end
|
||||
formattedCategories.push({
|
||||
value: 'all',
|
||||
label: 'All',
|
||||
count: totalAgents,
|
||||
description: 'All available agents',
|
||||
});
|
||||
|
||||
res.status(200).json(formattedCategories);
|
||||
} catch (error) {
|
||||
logger.error('[/Agents/Marketplace] Error fetching agent categories:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch agent categories',
|
||||
userMessage: 'Unable to load categories. Please refresh the page.',
|
||||
suggestion: 'Try refreshing the page or check your network connection',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all agents with pagination (for "all" category)
|
||||
* This is an alias for getPromotedAgents with showAll=true for backwards compatibility
|
||||
*
|
||||
* @param {Object} req - Express request object
|
||||
* @param {Object} res - Express response object
|
||||
*/
|
||||
const getAllAgents = async (req, res) => {
|
||||
// Set showAll parameter and delegate to getPromotedAgents
|
||||
req.query.showAll = 'true';
|
||||
return getPromotedAgents(req, res);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getPromotedAgents,
|
||||
getAgentsByCategory,
|
||||
searchAgents,
|
||||
getAgentCategories,
|
||||
getAllAgents,
|
||||
};
|
||||
|
|
@ -17,6 +17,8 @@ const {
|
|||
updateAgent,
|
||||
deleteAgent,
|
||||
getListAgentsByAccess,
|
||||
countPromotedAgents,
|
||||
revertAgentVersion,
|
||||
} = require('~/models/Agent');
|
||||
const {
|
||||
grantPermission,
|
||||
|
|
@ -30,8 +32,8 @@ const { refreshS3Url } = require('~/server/services/Files/S3/crud');
|
|||
const { filterFile } = require('~/server/services/Files/process');
|
||||
const { updateAction, getActions } = require('~/models/Action');
|
||||
const { getCachedTools } = require('~/server/services/Config');
|
||||
const { revertAgentVersion } = require('~/models/Agent');
|
||||
const { deleteFileByFilter } = require('~/models/File');
|
||||
const { getCategoriesWithCounts } = require('~/models');
|
||||
|
||||
const systemTools = {
|
||||
[Tools.execute_code]: true,
|
||||
|
|
@ -45,7 +47,7 @@ const systemTools = {
|
|||
* @param {ServerRequest} req - The request object.
|
||||
* @param {AgentCreateParams} req.body - The request body.
|
||||
* @param {ServerResponse} res - The response object.
|
||||
* @returns {Agent} 201 - success response - application/json
|
||||
* @returns {Promise<Agent>} 201 - success response - application/json
|
||||
*/
|
||||
const createAgentHandler = async (req, res) => {
|
||||
try {
|
||||
|
|
@ -402,12 +404,43 @@ const deleteAgentHandler = async (req, res) => {
|
|||
const getListAgentsHandler = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const { category, search, limit, cursor, promoted } = req.query;
|
||||
let requiredPermission = req.query.requiredPermission;
|
||||
if (typeof requiredPermission === 'string') {
|
||||
requiredPermission = parseInt(requiredPermission, 10);
|
||||
if (isNaN(requiredPermission)) {
|
||||
requiredPermission = PermissionBits.VIEW;
|
||||
}
|
||||
} else if (typeof requiredPermission !== 'number') {
|
||||
requiredPermission = PermissionBits.VIEW;
|
||||
}
|
||||
// Base filter
|
||||
const filter = {};
|
||||
|
||||
// Handle category filter - only apply if category is defined
|
||||
if (category !== undefined && category.trim() !== '') {
|
||||
filter.category = category;
|
||||
}
|
||||
|
||||
// Handle promoted filter - only from query param
|
||||
if (promoted === '1') {
|
||||
filter.is_promoted = true;
|
||||
} else if (promoted === '0') {
|
||||
filter.is_promoted = { $ne: true };
|
||||
}
|
||||
|
||||
// Handle search filter
|
||||
if (search && search.trim() !== '') {
|
||||
filter.$or = [
|
||||
{ name: { $regex: search.trim(), $options: 'i' } },
|
||||
{ description: { $regex: search.trim(), $options: 'i' } },
|
||||
];
|
||||
}
|
||||
// Get agent IDs the user has VIEW access to via ACL
|
||||
const accessibleIds = await findAccessibleResources({
|
||||
userId,
|
||||
resourceType: 'agent',
|
||||
requiredPermissions: PermissionBits.VIEW,
|
||||
requiredPermissions: requiredPermission,
|
||||
});
|
||||
const publiclyAccessibleIds = await findPubliclyAccessibleResources({
|
||||
resourceType: 'agent',
|
||||
|
|
@ -416,7 +449,9 @@ const getListAgentsHandler = async (req, res) => {
|
|||
// Use the new ACL-aware function
|
||||
const data = await getListAgentsByAccess({
|
||||
accessibleIds,
|
||||
otherParams: {}, // Can add query params here if needed
|
||||
otherParams: filter,
|
||||
limit,
|
||||
after: cursor,
|
||||
});
|
||||
if (data?.data?.length) {
|
||||
data.data = data.data.map((agent) => {
|
||||
|
|
@ -592,7 +627,48 @@ const revertAgentVersionHandler = async (req, res) => {
|
|||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Get all agent categories with counts
|
||||
*
|
||||
* @param {Object} _req - Express request object (unused)
|
||||
* @param {Object} res - Express response object
|
||||
*/
|
||||
const getAgentCategories = async (_req, res) => {
|
||||
try {
|
||||
const categories = await getCategoriesWithCounts();
|
||||
const promotedCount = await countPromotedAgents();
|
||||
const formattedCategories = categories.map((category) => ({
|
||||
value: category.value,
|
||||
label: category.label,
|
||||
count: category.agentCount,
|
||||
description: category.description,
|
||||
}));
|
||||
|
||||
if (promotedCount > 0) {
|
||||
formattedCategories.unshift({
|
||||
value: 'promoted',
|
||||
label: 'Promoted',
|
||||
count: promotedCount,
|
||||
description: 'Our recommended agents',
|
||||
});
|
||||
}
|
||||
|
||||
formattedCategories.push({
|
||||
value: 'all',
|
||||
label: 'All',
|
||||
description: 'All available agents',
|
||||
});
|
||||
|
||||
res.status(200).json(formattedCategories);
|
||||
} catch (error) {
|
||||
logger.error('[/Agents/Marketplace] Error fetching agent categories:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch agent categories',
|
||||
userMessage: 'Unable to load categories. Please refresh the page.',
|
||||
suggestion: 'Try refreshing the page or check your network connection',
|
||||
});
|
||||
}
|
||||
};
|
||||
module.exports = {
|
||||
createAgent: createAgentHandler,
|
||||
getAgent: getAgentHandler,
|
||||
|
|
@ -602,4 +678,5 @@ module.exports = {
|
|||
getListAgents: getListAgentsHandler,
|
||||
uploadAgentAvatar: uploadAgentAvatarHandler,
|
||||
revertAgentVersion: revertAgentVersionHandler,
|
||||
getAgentCategories,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -235,6 +235,81 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
|||
expect(agentInDb.tool_resources.invalid_resource).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should handle support_contact with empty strings', async () => {
|
||||
const dataWithEmptyContact = {
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
name: 'Agent with Empty Contact',
|
||||
support_contact: {
|
||||
name: '',
|
||||
email: '',
|
||||
},
|
||||
};
|
||||
|
||||
mockReq.body = dataWithEmptyContact;
|
||||
|
||||
await createAgentHandler(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
|
||||
const createdAgent = mockRes.json.mock.calls[0][0];
|
||||
expect(createdAgent.name).toBe('Agent with Empty Contact');
|
||||
expect(createdAgent.support_contact).toBeDefined();
|
||||
expect(createdAgent.support_contact.name).toBe('');
|
||||
expect(createdAgent.support_contact.email).toBe('');
|
||||
});
|
||||
|
||||
test('should handle support_contact with valid email', async () => {
|
||||
const dataWithValidContact = {
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
name: 'Agent with Valid Contact',
|
||||
support_contact: {
|
||||
name: 'Support Team',
|
||||
email: 'support@example.com',
|
||||
},
|
||||
};
|
||||
|
||||
mockReq.body = dataWithValidContact;
|
||||
|
||||
await createAgentHandler(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
|
||||
const createdAgent = mockRes.json.mock.calls[0][0];
|
||||
expect(createdAgent.support_contact).toBeDefined();
|
||||
expect(createdAgent.support_contact.name).toBe('Support Team');
|
||||
expect(createdAgent.support_contact.email).toBe('support@example.com');
|
||||
});
|
||||
|
||||
test('should reject support_contact with invalid email', async () => {
|
||||
const dataWithInvalidEmail = {
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
name: 'Agent with Invalid Email',
|
||||
support_contact: {
|
||||
name: 'Support',
|
||||
email: 'not-an-email',
|
||||
},
|
||||
};
|
||||
|
||||
mockReq.body = dataWithInvalidEmail;
|
||||
|
||||
await createAgentHandler(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
error: 'Invalid request data',
|
||||
details: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: ['support_contact', 'email'],
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle avatar validation', async () => {
|
||||
const dataWithAvatar = {
|
||||
provider: 'openai',
|
||||
|
|
@ -372,52 +447,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
|||
expect(agentInDb.id).toBe(existingAgentId);
|
||||
});
|
||||
|
||||
test('should reject update from non-author when not collaborative', async () => {
|
||||
const differentUserId = new mongoose.Types.ObjectId().toString();
|
||||
mockReq.user.id = differentUserId; // Different user
|
||||
mockReq.params.id = existingAgentId;
|
||||
mockReq.body = {
|
||||
name: 'Unauthorized Update',
|
||||
};
|
||||
|
||||
await updateAgentHandler(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(403);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({
|
||||
error: 'You do not have permission to modify this non-collaborative agent',
|
||||
});
|
||||
|
||||
// Verify agent was not modified in database
|
||||
const agentInDb = await Agent.findOne({ id: existingAgentId });
|
||||
expect(agentInDb.name).toBe('Original Agent');
|
||||
});
|
||||
|
||||
test('should allow update from non-author when collaborative', async () => {
|
||||
// First make the agent collaborative
|
||||
await Agent.updateOne({ id: existingAgentId }, { isCollaborative: true });
|
||||
|
||||
const differentUserId = new mongoose.Types.ObjectId().toString();
|
||||
mockReq.user.id = differentUserId; // Different user
|
||||
mockReq.params.id = existingAgentId;
|
||||
mockReq.body = {
|
||||
name: 'Collaborative Update',
|
||||
};
|
||||
|
||||
await updateAgentHandler(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).not.toHaveBeenCalledWith(403);
|
||||
expect(mockRes.json).toHaveBeenCalled();
|
||||
|
||||
const updatedAgent = mockRes.json.mock.calls[0][0];
|
||||
expect(updatedAgent.name).toBe('Collaborative Update');
|
||||
// Author field should be removed for non-author
|
||||
expect(updatedAgent.author).toBeUndefined();
|
||||
|
||||
// Verify in database
|
||||
const agentInDb = await Agent.findOne({ id: existingAgentId });
|
||||
expect(agentInDb.name).toBe('Collaborative Update');
|
||||
});
|
||||
|
||||
test('should allow admin to update any agent', async () => {
|
||||
const adminUserId = new mongoose.Types.ObjectId().toString();
|
||||
mockReq.user.id = adminUserId;
|
||||
|
|
@ -577,45 +606,6 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
|||
expect(agentInDb.__v).not.toBe(99);
|
||||
});
|
||||
|
||||
test('should prevent privilege escalation through isCollaborative', async () => {
|
||||
// Create a non-collaborative agent
|
||||
const authorId = new mongoose.Types.ObjectId();
|
||||
const agent = await Agent.create({
|
||||
id: `agent_${uuidv4()}`,
|
||||
name: 'Private Agent',
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
author: authorId,
|
||||
isCollaborative: false,
|
||||
versions: [
|
||||
{
|
||||
name: 'Private Agent',
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Try to make it collaborative as a different user
|
||||
const attackerId = new mongoose.Types.ObjectId().toString();
|
||||
mockReq.user.id = attackerId;
|
||||
mockReq.params.id = agent.id;
|
||||
mockReq.body = {
|
||||
isCollaborative: true, // Trying to escalate privileges
|
||||
};
|
||||
|
||||
await updateAgentHandler(mockReq, mockRes);
|
||||
|
||||
// Should be rejected
|
||||
expect(mockRes.status).toHaveBeenCalledWith(403);
|
||||
|
||||
// Verify in database that it's still not collaborative
|
||||
const agentInDb = await Agent.findOne({ id: agent.id });
|
||||
expect(agentInDb.isCollaborative).toBe(false);
|
||||
});
|
||||
|
||||
test('should prevent author hijacking', async () => {
|
||||
const originalAuthorId = new mongoose.Types.ObjectId();
|
||||
const attackerId = new mongoose.Types.ObjectId();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue