👀 feat: Add Agent Management Read Endpoints (#15561)

* feat: add Agent Management read endpoints

* fix: protect Agent management configuration reads

* fix: type Agent management list filters

* test: use typed Agent management assertions
This commit is contained in:
Dustin Healy 2026-09-03 18:16:14 -07:00 • committed by GitHub
parent a6cf61abcf
commit 2fe872d2ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 693 additions and 4 deletions

View file

@ -5,6 +5,12 @@ const mockMapAgentManagementError = jest.fn(() => ({
status: 404,
body: { error: { code: 'not_found', message: 'Agent not found' } },
}));
const mockList = jest.fn((_req, res) => res.status(200).json({ object: 'list', data: [] }));
const mockGet = jest.fn((_req, res) => res.status(200).json({ id: 'agent-one' }));
const mockCreateAgentManagementReadHandlers = jest.fn(() => ({
list: mockList,
get: mockGet,
}));
const mockCheckBan = jest.fn((_req, _res, next) => next());
const mockUaParser = jest.fn((_req, _res, next) => next());
@ -21,11 +27,22 @@ jest.mock('../middleware', () => ({
}));
jest.mock('@librechat/api', () => ({
mapAgentManagementError: mockMapAgentManagementError,
createAgentManagementReadHandlers: mockCreateAgentManagementReadHandlers,
}));
jest.mock('~/server/middleware', () => ({
checkBan: mockCheckBan,
uaParser: mockUaParser,
}));
jest.mock('~/server/middleware/roles/capabilities', () => ({ hasCapability: jest.fn() }));
jest.mock('~/server/services/PermissionService', () => ({
checkPermission: jest.fn(),
findAccessibleResources: jest.fn(),
}));
jest.mock('~/models', () => ({
getRoleByName: jest.fn(),
getAgentWithVersionCount: jest.fn(),
getAgentManagementListByAccess: jest.fn(),
}));
const router = require('../management');
@ -51,7 +68,7 @@ describe('Agent Management route boundary', () => {
it('allows an authenticated non-browser client into the management router', async () => {
const response = await request(app)
.get('/api/agents/v1/agents/not-a-management-operation')
.post('/api/agents/v1/agents/not-a-management-operation')
.set('Authorization', 'Bearer valid-token')
.set('User-Agent', 'curl/8.0.0');
@ -62,4 +79,18 @@ describe('Agent Management route boundary', () => {
expect(mockMapAgentManagementError).toHaveBeenCalledWith('not_found');
expect(mockRequireAgentManagementAuth).toHaveBeenCalledTimes(1);
});
it('dispatches authenticated list and retrieve requests to the management read handlers', async () => {
const listResponse = await request(app)
.get('/api/agents/v1/agents')
.set('Authorization', 'Bearer valid-token');
const getResponse = await request(app)
.get('/api/agents/v1/agents/agent-one')
.set('Authorization', 'Bearer valid-token');
expect(listResponse.status).toBe(200);
expect(getResponse.status).toBe(200);
expect(mockList).toHaveBeenCalledTimes(1);
expect(mockGet).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,13 +1,27 @@
const express = require('express');
const { mapAgentManagementError } = require('@librechat/api');
const { createAgentManagementReadHandlers, mapAgentManagementError } = require('@librechat/api');
const { checkBan } = require('~/server/middleware');
const { hasCapability } = require('~/server/middleware/roles/capabilities');
const { checkPermission, findAccessibleResources } = require('~/server/services/PermissionService');
const db = require('~/models');
const { requireAgentManagementAuth } = require('./middleware');
const router = express.Router();
const handlers = createAgentManagementReadHandlers({
getRoleByName: db.getRoleByName,
getAgentWithVersionCount: db.getAgentWithVersionCount,
getAgentManagementListByAccess: db.getAgentManagementListByAccess,
findAccessibleResources,
checkPermission,
hasCapability,
});
router.use(requireAgentManagementAuth);
router.use(checkBan);
router.get('/', handlers.list);
router.get('/:id', handlers.get);
router.use((_req, res) => {
const { status, body } = mapAgentManagementError('not_found');
return res.status(status).json(body);

View file

@ -28,6 +28,7 @@ export * from './lazySubagents';
export * from './lazyHistory';
export * from './memory';
export * from './management';
export * from './reads';
export * from './mcpIdentity';
export * from './orphans';
export * from './migration';

View file

@ -167,6 +167,16 @@ describe('Agent Management contract', () => {
expect(response).not.toHaveProperty('is_promoted');
});
it('omits legacy string avatars that are not part of the management contract', () => {
const response = projectAgentManagementResponse({
...persistedAgent,
avatar: 'https://example.com/legacy-avatar.png',
});
expect(JSON.parse(JSON.stringify(response))).not.toHaveProperty('avatar');
expect(agentManagementResponseSchema.parse(response)).toEqual(response);
});
it('does not apply the current request admission limit to persisted subagents', () => {
const subagents = {
enabled: true,

View file

@ -28,11 +28,15 @@ export type AgentManagementResponse = Omit<
updatedAt: string;
};
export type AgentManagementProjectionSource = Partial<
Omit<AgentManagementResponse, 'id' | 'provider' | 'model' | 'version' | 'createdAt' | 'updatedAt'>
Omit<
AgentManagementResponse,
'id' | 'provider' | 'model' | 'version' | 'createdAt' | 'updatedAt' | 'avatar'
>
> & {
id?: string;
provider?: string;
model?: string | null;
avatar?: AgentManagementResponse['avatar'] | string;
version?: number;
versions?: readonly object[];
createdAt?: string | Date;
@ -225,7 +229,7 @@ export function projectAgentManagementResponse(
name: source.name,
description: source.description,
instructions: source.instructions,
avatar: source.avatar,
avatar: typeof source.avatar === 'string' ? undefined : source.avatar,
model_parameters: source.model_parameters,
tools: source.tools,
skills: source.skills,

View file

@ -0,0 +1,208 @@
import { Types } from 'mongoose';
import { SystemCapabilities } from '@librechat/data-schemas';
import {
Permissions,
PermissionBits,
PermissionTypes,
ResourceType,
} from 'librechat-data-provider';
import type { IRole, IUser } from '@librechat/data-schemas';
import type { Request, Response } from 'express';
import type { AgentManagementReadDeps } from './reads';
import { createAgentManagementReadHandlers } from './reads';
jest.mock('@librechat/data-schemas', () => {
const actual = jest.requireActual('@librechat/data-schemas');
return {
...actual,
logger: { warn: jest.fn(), error: jest.fn() },
};
});
const tenantId = 'tenant-a';
const user = {
id: new Types.ObjectId().toString(),
tenantId,
role: 'USER',
idOnTheSource: 'external-user',
} as IUser;
const objectId = new Types.ObjectId();
const agent = {
_id: objectId,
id: 'agent-one',
provider: 'openAI',
model: 'gpt-5',
name: 'Agent One',
version: 2,
createdAt: new Date('2026-09-01T10:00:00.000Z'),
updatedAt: new Date('2026-09-02T10:00:00.000Z'),
};
function makeRequest(overrides: Partial<Request> = {}): Request {
return { user, query: {}, params: {}, ...overrides } as Request;
}
function makeResponse(): Response {
const response = {
status: jest.fn(),
json: jest.fn(),
};
response.status.mockReturnValue(response);
response.json.mockReturnValue(response);
return response as unknown as Response;
}
function makeDeps(overrides: Partial<AgentManagementReadDeps> = {}): AgentManagementReadDeps {
return {
getRoleByName: jest.fn().mockResolvedValue({
permissions: { [PermissionTypes.AGENTS]: { [Permissions.USE]: true } },
} as unknown as IRole),
getAgentWithVersionCount: jest.fn().mockResolvedValue(agent),
getAgentManagementListByAccess: jest.fn().mockResolvedValue({
data: [agent],
has_more: false,
after: null,
}),
findAccessibleResources: jest.fn().mockResolvedValue([objectId]),
checkPermission: jest.fn().mockResolvedValue(true),
hasCapability: jest.fn().mockResolvedValue(false),
...overrides,
};
}
describe('Agent Management read handlers', () => {
it('lists only ACL-discovered records in the authenticated tenant', async () => {
const deps = makeDeps();
const response = makeResponse();
await createAgentManagementReadHandlers(deps).list(
makeRequest({ query: { limit: '10' } }),
response,
);
expect(deps.findAccessibleResources).toHaveBeenCalledWith({
userId: user.id,
role: user.role,
idOnTheSource: user.idOnTheSource,
resourceType: ResourceType.AGENT,
requiredPermissions: PermissionBits.EDIT,
});
expect(deps.getAgentManagementListByAccess).toHaveBeenCalledWith({
accessibleIds: [objectId],
tenantId,
limit: 10,
after: undefined,
});
expect(response.status).toHaveBeenCalledWith(200);
expect(response.json).toHaveBeenCalledWith(
expect.objectContaining({
object: 'list',
data: [expect.objectContaining({ id: 'agent-one' })],
}),
);
});
it('rejects malformed cursors before ACL or persistence work', async () => {
const deps = makeDeps();
const response = makeResponse();
await createAgentManagementReadHandlers(deps).list(
makeRequest({ query: { cursor: 'not-a-cursor' } }),
response,
);
expect(response.status).toHaveBeenCalledWith(400);
expect(deps.findAccessibleResources).not.toHaveBeenCalled();
expect(deps.getAgentManagementListByAccess).not.toHaveBeenCalled();
});
it('retrieves by public ID and authenticated tenant before applying EDIT access', async () => {
const deps = makeDeps();
const response = makeResponse();
await createAgentManagementReadHandlers(deps).get(
makeRequest({ params: { id: 'agent-one' } }),
response,
);
expect(deps.getAgentWithVersionCount).toHaveBeenCalledWith({ id: 'agent-one', tenantId });
expect(deps.checkPermission).toHaveBeenCalledWith({
userId: user.id,
role: user.role,
resourceType: ResourceType.AGENT,
resourceId: objectId,
requiredPermission: PermissionBits.EDIT,
});
expect(response.status).toHaveBeenCalledWith(200);
expect(response.json).toHaveBeenCalledWith(expect.objectContaining({ id: 'agent-one' }));
});
it('returns not found when the tenant-scoped lookup cannot resolve the ID', async () => {
const deps = makeDeps({ getAgentWithVersionCount: jest.fn().mockResolvedValue(null) });
const response = makeResponse();
await createAgentManagementReadHandlers(deps).get(
makeRequest({ params: { id: 'agent-in-another-tenant' } }),
response,
);
expect(response.status).toHaveBeenCalledWith(404);
expect(deps.checkPermission).not.toHaveBeenCalled();
});
it('denies a known Agent when neither EDIT ACL nor the management capability is held', async () => {
const deps = makeDeps({ checkPermission: jest.fn().mockResolvedValue(false) });
const response = makeResponse();
await createAgentManagementReadHandlers(deps).get(
makeRequest({ params: { id: 'agent-one' } }),
response,
);
expect(response.status).toHaveBeenCalledWith(403);
});
it('uses the existing manage-agents capability as the resource ACL bypass', async () => {
const deps = makeDeps({ hasCapability: jest.fn().mockResolvedValue(true) });
const response = makeResponse();
await createAgentManagementReadHandlers(deps).get(
makeRequest({ params: { id: 'agent-one' } }),
response,
);
expect(deps.hasCapability).toHaveBeenCalledWith(user, SystemCapabilities.MANAGE_AGENTS);
expect(deps.checkPermission).not.toHaveBeenCalled();
expect(response.status).toHaveBeenCalledWith(200);
});
it('lists every Agent in the tenant for the existing manage-agents capability', async () => {
const deps = makeDeps({ hasCapability: jest.fn().mockResolvedValue(true) });
const response = makeResponse();
await createAgentManagementReadHandlers(deps).list(makeRequest(), response);
expect(deps.findAccessibleResources).not.toHaveBeenCalled();
expect(deps.getAgentManagementListByAccess).toHaveBeenCalledWith({
accessibleIds: null,
tenantId,
limit: 20,
after: undefined,
});
expect(response.status).toHaveBeenCalledWith(200);
});
it('requires the same AGENTS:USE role permission as browser reads', async () => {
const deps = makeDeps({
getRoleByName: jest.fn().mockResolvedValue({
permissions: { [PermissionTypes.AGENTS]: { [Permissions.USE]: false } },
} as never),
});
const response = makeResponse();
await createAgentManagementReadHandlers(deps).list(makeRequest(), response);
expect(response.status).toHaveBeenCalledWith(403);
expect(deps.findAccessibleResources).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,188 @@
import { logger, ResourceCapabilityMap } from '@librechat/data-schemas';
import {
PermissionBits,
Permissions,
PermissionTypes,
ResourceType,
} from 'librechat-data-provider';
import type { IRole, IUser, SystemCapability } from '@librechat/data-schemas';
import type { Request, Response } from 'express';
import type { Types } from 'mongoose';
import type { AgentManagementProjectionSource } from './management';
import {
agentManagementListSchema,
mapAgentManagementError,
projectAgentManagementListResponse,
projectAgentManagementResponse,
} from './management';
import { checkAccessWithRequestCache } from '../middleware/access';
type AgentManagementRecord = AgentManagementProjectionSource & { _id: Types.ObjectId };
export interface AgentManagementReadDeps {
getRoleByName: (roleName: string, fieldsToSelect?: string | string[]) => Promise<IRole | null>;
getAgentWithVersionCount: (search: {
id: string;
tenantId: string;
}) => Promise<AgentManagementRecord | null>;
getAgentManagementListByAccess: (params: {
accessibleIds: Types.ObjectId[] | null;
tenantId: string;
limit: number;
after?: string | null;
}) => Promise<{
data: AgentManagementRecord[];
has_more: boolean;
after: string | null;
}>;
findAccessibleResources: (params: {
userId: string;
role?: string;
idOnTheSource?: string;
resourceType: ResourceType;
requiredPermissions: PermissionBits;
}) => Promise<Types.ObjectId[]>;
checkPermission: (params: {
userId: string;
role?: string;
resourceType: ResourceType;
resourceId: Types.ObjectId;
requiredPermission: PermissionBits;
}) => Promise<boolean>;
hasCapability: (user: IUser, capability: SystemCapability) => Promise<boolean>;
}
function sendError(
res: Response,
code: Parameters<typeof mapAgentManagementError>[0],
error?: unknown,
) {
const mapped = mapAgentManagementError(code, error);
return res.status(mapped.status).json(mapped.body);
}
async function canUseAgents(req: Request, user: IUser, deps: AgentManagementReadDeps) {
return await checkAccessWithRequestCache({
req,
user,
permissionType: PermissionTypes.AGENTS,
permissions: [Permissions.USE],
getRoleByName: deps.getRoleByName,
});
}
async function canViewAgent(
user: IUser,
agent: AgentManagementRecord,
deps: AgentManagementReadDeps,
canManageAll: boolean,
) {
if (canManageAll) {
return true;
}
return await deps.checkPermission({
userId: user.id,
role: user.role,
resourceType: ResourceType.AGENT,
resourceId: agent._id,
requiredPermission: PermissionBits.EDIT,
});
}
async function hasManageAgentsCapability(user: IUser, deps: AgentManagementReadDeps) {
const capability = ResourceCapabilityMap[ResourceType.AGENT];
try {
if (capability != null && (await deps.hasCapability(user, capability))) {
return true;
}
} catch (error) {
logger.warn(
`[AgentManagement] Agent capability check failed, denying bypass: ${(error as Error).message}`,
);
}
return false;
}
/** Typed management read handlers; the Express route supplies concrete database and ACL dependencies. */
export function createAgentManagementReadHandlers(deps: AgentManagementReadDeps): {
list: (req: Request, res: Response) => Promise<Response>;
get: (req: Request, res: Response) => Promise<Response>;
} {
async function list(req: Request, res: Response): Promise<Response> {
const parsedQuery = agentManagementListSchema.safeParse(req.query);
if (!parsedQuery.success) {
return sendError(res, 'invalid_request', parsedQuery.error);
}
try {
const user = req.user as IUser | undefined;
if (!user?.id || !user.tenantId) {
return sendError(res, 'permission_denied');
}
const [canUse, canManageAll] = await Promise.all([
canUseAgents(req, user, deps),
hasManageAgentsCapability(user, deps),
]);
if (!canUse) {
return sendError(res, 'permission_denied');
}
const accessibleIds = canManageAll
? null
: await deps.findAccessibleResources({
userId: user.id,
role: user.role,
idOnTheSource: user.idOnTheSource,
resourceType: ResourceType.AGENT,
requiredPermissions: PermissionBits.EDIT,
});
const result = await deps.getAgentManagementListByAccess({
accessibleIds,
tenantId: user.tenantId,
limit: parsedQuery.data.limit,
after: parsedQuery.data.cursor,
});
return res.status(200).json(projectAgentManagementListResponse(result));
} catch (error) {
logger.error('[AgentManagement] Error listing Agents', error);
return sendError(res, 'internal_error');
}
}
async function get(req: Request, res: Response): Promise<Response> {
try {
const user = req.user as IUser | undefined;
if (!user?.id || !user.tenantId) {
return sendError(res, 'permission_denied');
}
const [canUse, canManageAll, agent] = await Promise.all([
canUseAgents(req, user, deps),
hasManageAgentsCapability(user, deps),
deps.getAgentWithVersionCount({
id: req.params.id,
tenantId: user.tenantId,
}),
]);
if (!canUse) {
return sendError(res, 'permission_denied');
}
if (!agent) {
return sendError(res, 'not_found');
}
if (!(await canViewAgent(user, agent, deps, canManageAll))) {
return sendError(res, 'permission_denied');
}
return res.status(200).json(projectAgentManagementResponse(agent));
} catch (error) {
logger.error('[AgentManagement] Error retrieving Agent', error);
return sendError(res, 'internal_error');
}
}
return { list, get };
}

View file

@ -61,6 +61,7 @@ let addAgentResourceFile: AgentMethods['addAgentResourceFile'];
let removeAgentResourceFiles: AgentMethods['removeAgentResourceFiles'];
let removeAgentResourceFilesFromAllAgents: AgentMethods['removeAgentResourceFilesFromAllAgents'];
let getListAgentsByAccess: AgentMethods['getListAgentsByAccess'];
let getAgentManagementListByAccess: AgentMethods['getAgentManagementListByAccess'];
let generateActionMetadataHash: AgentMethods['generateActionMetadataHash'];
const getActions = jest.fn().mockResolvedValue([]);
@ -108,6 +109,7 @@ beforeAll(async () => {
removeAgentResourceFiles = methods.removeAgentResourceFiles;
removeAgentResourceFilesFromAllAgents = methods.removeAgentResourceFilesFromAllAgents;
getListAgentsByAccess = methods.getListAgentsByAccess;
getAgentManagementListByAccess = methods.getAgentManagementListByAccess;
generateActionMetadataHash = methods.generateActionMetadataHash;
await mongoose.connect(mongoUri);
@ -4733,6 +4735,153 @@ describe('Support Contact Field', () => {
});
});
describe('getAgentManagementListByAccess', () => {
beforeEach(async () => {
await Agent.deleteMany({});
});
it('returns full configuration with version counts without changing updatedAt', async () => {
const tenantId = `tenant-${uuidv4()}`;
const created = await tenantStorage.run({ tenantId }, () =>
createAgent({
id: `agent_${uuidv4()}`,
name: 'Managed Agent',
instructions: 'Keep this configuration',
provider: 'openai',
model: 'gpt-4',
author: new mongoose.Types.ObjectId(),
tools: ['web_search'],
}),
);
await tenantStorage.run({ tenantId }, () =>
updateAgent({ id: created.id }, { instructions: 'Updated configuration' }),
);
const before = await Agent.findById(created._id)
.select({ updatedAt: 1 })
.lean<{ updatedAt?: Date }>();
const beforeUpdatedAt = before?.updatedAt;
const result = await tenantStorage.run({ tenantId }, () =>
getAgentManagementListByAccess({
accessibleIds: [created._id],
tenantId,
limit: 20,
}),
);
const after = await Agent.findById(created._id)
.select({ updatedAt: 1 })
.lean<{ updatedAt?: Date }>();
expect(result.data).toHaveLength(1);
expect(result.data[0]).toMatchObject({
id: created.id,
instructions: 'Updated configuration',
tools: ['web_search'],
version: 2,
});
expect(result.data[0].versions).toBeUndefined();
expect(after?.updatedAt).toEqual(beforeUpdatedAt);
});
it('enforces tenant scope even when accessible IDs contain another tenant record', async () => {
const tenantA = `tenant-a-${uuidv4()}`;
const tenantB = `tenant-b-${uuidv4()}`;
const publicId = `agent_${uuidv4()}`;
const author = new mongoose.Types.ObjectId();
const agentA = await tenantStorage.run({ tenantId: tenantA }, () =>
createAgent({ id: publicId, name: 'A', provider: 'openai', model: 'gpt-4', author }),
);
const agentB = await tenantStorage.run({ tenantId: tenantB }, () =>
createAgent({ id: publicId, name: 'B', provider: 'openai', model: 'gpt-4', author }),
);
const result = await tenantStorage.run({ tenantId: tenantA }, () =>
getAgentManagementListByAccess({
accessibleIds: [agentA._id, agentB._id],
tenantId: tenantA,
limit: 20,
}),
);
expect(result.data.map(({ name }) => name)).toEqual(['A']);
});
it('keeps an unrestricted capability listing inside the authenticated tenant', async () => {
const tenantA = `tenant-a-${uuidv4()}`;
const tenantB = `tenant-b-${uuidv4()}`;
const author = new mongoose.Types.ObjectId();
await tenantStorage.run({ tenantId: tenantA }, () =>
createAgent({
id: `agent_${uuidv4()}`,
name: 'A',
provider: 'openai',
model: 'gpt-4',
author,
}),
);
await tenantStorage.run({ tenantId: tenantB }, () =>
createAgent({
id: `agent_${uuidv4()}`,
name: 'B',
provider: 'openai',
model: 'gpt-4',
author,
}),
);
const result = await tenantStorage.run({ tenantId: tenantA }, () =>
getAgentManagementListByAccess({
accessibleIds: null,
tenantId: tenantA,
limit: 20,
}),
);
expect(result.data.map(({ name }) => name)).toEqual(['A']);
});
it('paginates deterministically without overlap', async () => {
const tenantId = `tenant-${uuidv4()}`;
const author = new mongoose.Types.ObjectId();
const agents = await tenantStorage.run({ tenantId }, () =>
Promise.all(
['A', 'B', 'C'].map((name) =>
createAgent({
id: `agent_${uuidv4()}`,
name,
provider: 'openai',
model: 'gpt-4',
author,
}),
),
),
);
const first = await tenantStorage.run({ tenantId }, () =>
getAgentManagementListByAccess({
accessibleIds: agents.map(({ _id }) => _id),
tenantId,
limit: 2,
}),
);
const second = await tenantStorage.run({ tenantId }, () =>
getAgentManagementListByAccess({
accessibleIds: agents.map(({ _id }) => _id),
tenantId,
limit: 2,
after: first.after,
}),
);
expect(first.data).toHaveLength(2);
expect(first.has_more).toBe(true);
expect(first.after).toBeTruthy();
expect(second.data).toHaveLength(1);
expect(second.has_more).toBe(false);
expect(first.data.map(({ id }) => id)).not.toContain(second.data[0].id);
});
});
function createBasicAgent(overrides = {}) {
const defaults = {
id: `agent_${uuidv4()}`,

View file

@ -532,6 +532,22 @@ export function createAgentMethods(
has_more: boolean;
after: string | null;
}>;
getAgentManagementListByAccess: ({
accessibleIds,
tenantId,
limit,
after,
}: {
/** `null` means the caller already passed the unrestricted management-capability check. */
accessibleIds: Types.ObjectId[] | null;
tenantId: string;
limit: number;
after?: string | null;
}) => Promise<{
data: Array<IAgent & { version: number; createdAt: Date; updatedAt: Date }>;
has_more: boolean;
after: string | null;
}>;
removeAgentResourceFiles: ({
agent_id,
files,
@ -1273,6 +1289,72 @@ export function createAgentMethods(
};
}
/**
* Returns the full Agent configuration required by the management response projector.
* Unlike the browser list path, this query performs no avatar refresh or persistence write.
*/
async function getAgentManagementListByAccess({
accessibleIds,
tenantId,
limit,
after = null,
}: {
/** `null` means the caller already passed the unrestricted management-capability check. */
accessibleIds: Types.ObjectId[] | null;
tenantId: string;
limit: number;
after?: string | null;
}): Promise<{
data: Array<IAgent & { version: number; createdAt: Date; updatedAt: Date }>;
has_more: boolean;
after: string | null;
}> {
const Agent = mongoose.models.Agent as Model<IAgent>;
const match: FilterQuery<IAgent> = {
tenantId,
...(accessibleIds != null ? { _id: { $in: accessibleIds } } : {}),
};
if (after) {
const cursor = JSON.parse(Buffer.from(after, 'base64').toString('utf8')) as {
updatedAt: string;
_id: string;
};
match.$or = [
{ updatedAt: { $lt: new Date(cursor.updatedAt) } },
{
updatedAt: new Date(cursor.updatedAt),
_id: { $gt: new mongoose.Types.ObjectId(cursor._id) },
},
];
}
const agents = await Agent.aggregate<
IAgent & { version: number; createdAt: Date; updatedAt: Date }
>([
{ $match: match },
{ $sort: { updatedAt: -1, _id: 1 } },
{ $limit: limit + 1 },
{ $addFields: { version: { $size: { $ifNull: ['$versions', []] } } } },
{ $project: { versions: 0 } },
]);
const hasMore = agents.length > limit;
const data = hasMore ? agents.slice(0, limit) : agents;
const lastAgent = data[data.length - 1];
const nextCursor =
hasMore && lastAgent
? Buffer.from(
JSON.stringify({
updatedAt: lastAgent.updatedAt.toISOString(),
_id: lastAgent._id.toString(),
}),
).toString('base64')
: null;
return { data, has_more: hasMore, after: nextCursor };
}
/**
* Reverts an agent to a specific version in its version history.
*/
@ -1396,6 +1478,7 @@ export function createAgentMethods(
countPromotedAgents,
addAgentResourceFile,
getListAgentsByAccess,
getAgentManagementListByAccess,
removeAgentResourceFiles,
generateActionMetadataHash,
removeAgentFromUserFavorites,

View file

@ -163,6 +163,7 @@ const agentSchema: Schema<IAgent> = new Schema<IAgent>(
agentSchema.index({ id: 1, tenantId: 1 }, { unique: true });
agentSchema.index({ mcpServerNames: 1, tenantId: 1 });
agentSchema.index({ updatedAt: -1, _id: 1 });
agentSchema.index({ tenantId: 1, updatedAt: -1, _id: 1 });
agentSchema.index({ 'edges.to': 1 });
export default agentSchema;