✏️ feat: Add Agent Management Update Endpoint (#15566)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions

* feat: add Agent Management update endpoint

* fix: reject unsupported Agent update values

* test: simplify Agent update mocks
This commit is contained in:
Dustin Healy 2026-09-03 21:16:57 -07:00 • committed by GitHub
parent 9a43362663
commit ab0d74b492
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 545 additions and 15 deletions

View file

@ -105,6 +105,7 @@ const {
createAgentManagementAuth,
createAgentManagementCreateHandler,
createAgentManagementReadHandlers,
createAgentManagementUpdateHandler,
mergeDeploymentSkillIds,
refreshS3Url,
} = require('@librechat/api');
@ -380,7 +381,7 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
expect(agentInDb.author.toString()).toBe(mockReq.user.id);
});
test('management creation can be read back through the authenticated management API', async () => {
test('management creation can be updated and read through the authenticated management API', async () => {
const tenantId = `tenant-${nanoid(8)}`;
const clientId = `client-${nanoid(8)}`;
const principal = await tenantStorage.run({ tenantId }, () => createOwner());
@ -435,6 +436,14 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
getRoleByName,
createAgent: createAgentHandler,
});
const checkEditPermission = async ({ userId: accessibleUserId, resourceType, resourceId }) =>
(await AclEntry.exists({
principalId: accessibleUserId,
resourceType,
resourceId,
permBits: { $bitsAllSet: PermissionBits.EDIT },
})) != null;
const hasCapability = jest.fn().mockResolvedValue(false);
const reads = createAgentManagementReadHandlers({
getRoleByName,
getAgentWithVersionCount: db.getAgentWithVersionCount,
@ -445,14 +454,15 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
resourceType,
permBits: { $bitsAllSet: PermissionBits.EDIT },
}),
checkPermission: async ({ userId: accessibleUserId, resourceType, resourceId }) =>
(await AclEntry.exists({
principalId: accessibleUserId,
resourceType,
resourceId,
permBits: { $bitsAllSet: PermissionBits.EDIT },
})) != null,
hasCapability: jest.fn().mockResolvedValue(false),
checkPermission: checkEditPermission,
hasCapability,
});
const update = createAgentManagementUpdateHandler({
getRoleByName,
getAgentWithVersionCount: db.getAgentWithVersionCount,
checkPermission: checkEditPermission,
hasCapability,
updateAgent: updateAgentHandler,
});
const app = express();
app.use(express.json());
@ -463,14 +473,21 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
});
app.get('/api/agents/v1/agents', reads.list);
app.get('/api/agents/v1/agents/:id', reads.get);
app.patch('/api/agents/v1/agents/:id', (req, res) => {
req.config = {};
return update(req, res);
});
const createdResponse = await request(app)
.post('/api/agents/v1/agents')
.set('Authorization', 'Bearer valid-token')
.send({
name: 'Managed Agent',
description: 'Description that remains unchanged',
provider: 'openai',
model: 'gpt-4',
avatar: { filepath: 'avatars/managed.png', source: 'local' },
conversation_starters: ['Help me get started'],
});
expect(createdResponse.status).toBe(201);
@ -499,11 +516,63 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
expect(listedResponse.status).toBe(200);
expect(listedResponse.body.data).toEqual([createdResponse.body]);
const created = await tenantStorage.run({ tenantId }, () =>
Agent.findOne({ id: createdResponse.body.id }).lean(),
const otherTenantId = `tenant-${nanoid(8)}`;
await tenantStorage.run({ tenantId: otherTenantId }, () =>
Agent.create({
id: createdResponse.body.id,
name: 'Other tenant Agent',
provider: 'openai',
model: 'gpt-4',
author: new mongoose.Types.ObjectId(),
}),
);
const updatedResponse = await request(app)
.patch(`/api/agents/v1/agents/${createdResponse.body.id}`)
.set('Authorization', 'Bearer valid-token')
.send({
name: 'Updated Managed Agent',
avatar: null,
conversation_starters: [],
});
expect(updatedResponse.status).toBe(200);
expect(updatedResponse.body).toEqual(
expect.objectContaining({
id: createdResponse.body.id,
name: 'Updated Managed Agent',
description: 'Description that remains unchanged',
avatar: null,
conversation_starters: [],
version: 2,
}),
);
expect(updatedResponse.body).not.toHaveProperty('_id');
expect(updatedResponse.body).not.toHaveProperty('author');
expect(updatedResponse.body).not.toHaveProperty('tenantId');
const retrievedAfterUpdate = await request(app)
.get(`/api/agents/v1/agents/${createdResponse.body.id}`)
.set('Authorization', 'Bearer valid-token');
expect(retrievedAfterUpdate.status).toBe(200);
expect(retrievedAfterUpdate.body).toEqual(updatedResponse.body);
const created = await Agent.collection.findOne({
id: createdResponse.body.id,
tenantId,
});
const otherTenantAgent = await Agent.collection.findOne({
id: createdResponse.body.id,
tenantId: otherTenantId,
});
expect(created.tenantId).toBe(tenantId);
expect(created.author.toString()).toBe(userId);
expect(created.name).toBe('Updated Managed Agent');
expect(created.description).toBe('Description that remains unchanged');
expect(created.avatar).toBeNull();
expect(created.conversation_starters).toEqual([]);
expect(created.versions).toHaveLength(2);
expect(otherTenantAgent.name).toBe('Other tenant Agent');
expect(grantPermission).toHaveBeenCalledWith(
expect.objectContaining({
principalType: PrincipalType.USER,

View file

@ -17,7 +17,14 @@ const mockCreateAgentManagementCreateHandler = jest.fn((deps) => {
mockCreateDeps = deps;
return mockCreate;
});
const mockUpdate = jest.fn((_req, res) => res.status(200).json({ id: 'agent-updated' }));
let mockUpdateDeps;
const mockCreateAgentManagementUpdateHandler = jest.fn((deps) => {
mockUpdateDeps = deps;
return mockUpdate;
});
const mockBrowserCreate = jest.fn();
const mockBrowserUpdate = jest.fn();
const mockCheckBan = jest.fn((_req, _res, next) => next());
const mockConfigMiddleware = jest.fn((_req, _res, next) => next());
const mockUaParser = jest.fn((_req, _res, next) => next());
@ -37,13 +44,17 @@ jest.mock('@librechat/api', () => ({
mapAgentManagementError: mockMapAgentManagementError,
createAgentManagementCreateHandler: mockCreateAgentManagementCreateHandler,
createAgentManagementReadHandlers: mockCreateAgentManagementReadHandlers,
createAgentManagementUpdateHandler: mockCreateAgentManagementUpdateHandler,
}));
jest.mock('~/server/middleware', () => ({
checkBan: mockCheckBan,
configMiddleware: mockConfigMiddleware,
uaParser: mockUaParser,
}));
jest.mock('~/server/controllers/agents/v1', () => ({ createAgent: mockBrowserCreate }));
jest.mock('~/server/controllers/agents/v1', () => ({
createAgent: mockBrowserCreate,
updateAgent: mockBrowserUpdate,
}));
jest.mock('~/server/middleware/roles/capabilities', () => ({ hasCapability: jest.fn() }));
jest.mock('~/server/services/PermissionService', () => ({
checkPermission: jest.fn(),
@ -121,4 +132,21 @@ describe('Agent Management route boundary', () => {
expect(mockCreateDeps.getRoleByName).toEqual(expect.any(Function));
expect(mockCreateDeps.createAgent).toBe(mockBrowserCreate);
});
it('loads Agent configuration and dispatches authenticated update requests', async () => {
const response = await request(app)
.patch('/api/agents/v1/agents/agent-one')
.set('Authorization', 'Bearer valid-token')
.send({ name: 'Updated Agent' });
expect(response.status).toBe(200);
expect(response.body).toEqual({ id: 'agent-updated' });
expect(mockConfigMiddleware).toHaveBeenCalledTimes(1);
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdateDeps.getRoleByName).toEqual(expect.any(Function));
expect(mockUpdateDeps.getAgentWithVersionCount).toEqual(expect.any(Function));
expect(mockUpdateDeps.checkPermission).toEqual(expect.any(Function));
expect(mockUpdateDeps.hasCapability).toEqual(expect.any(Function));
expect(mockUpdateDeps.updateAgent).toBe(mockBrowserUpdate);
});
});

View file

@ -2,6 +2,7 @@ const express = require('express');
const {
createAgentManagementCreateHandler,
createAgentManagementReadHandlers,
createAgentManagementUpdateHandler,
mapAgentManagementError,
} = require('@librechat/api');
const { checkBan, configMiddleware } = require('~/server/middleware');
@ -24,6 +25,13 @@ const createHandler = createAgentManagementCreateHandler({
getRoleByName: db.getRoleByName,
createAgent: v1.createAgent,
});
const updateHandler = createAgentManagementUpdateHandler({
getRoleByName: db.getRoleByName,
getAgentWithVersionCount: db.getAgentWithVersionCount,
checkPermission,
hasCapability,
updateAgent: v1.updateAgent,
});
router.use(requireAgentManagementAuth);
router.use(checkBan);
@ -31,6 +39,7 @@ router.use(checkBan);
router.post('/', configMiddleware, createHandler);
router.get('/', readHandlers.list);
router.get('/:id', readHandlers.get);
router.patch('/:id', configMiddleware, updateHandler);
router.use((_req, res) => {
const { status, body } = mapAgentManagementError('not_found');

View file

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

View file

@ -83,6 +83,22 @@ describe('Agent Management contract', () => {
agentManagementCreateSchema.safeParse({ provider: 'openAI', model: null }).success,
).toBe(false);
});
it.each([
{ name: null },
{ description: null },
{ instructions: null },
{ model: null },
{ avatar: { filepath: 'avatars/replacement.png', source: 'local' } },
])('rejects update values the browser update flow cannot apply: %p', (update) => {
expect(agentManagementUpdateSchema.safeParse(update).success).toBe(false);
});
it('accepts the explicitly supported update clears', () => {
expect(
agentManagementUpdateSchema.parse({ avatar: null, code_environment_id: null }),
).toEqual({ avatar: null, code_environment_id: null });
});
});
describe('pagination', () => {

View file

@ -16,7 +16,16 @@ export type AgentManagementCreate = Omit<z.output<typeof agentCreateSchema>, 'mo
type AgentManagementCreateInput = Omit<z.input<typeof agentCreateSchema>, 'model'> & {
model: string;
};
export type AgentManagementUpdate = z.output<typeof agentUpdateSchema>;
export type AgentManagementUpdate = Omit<
z.output<typeof agentUpdateSchema>,
'name' | 'description' | 'instructions' | 'model' | 'avatar'
> & {
name?: string;
description?: string;
instructions?: string;
model?: string;
avatar?: null;
};
export type AgentManagementList = {
limit: number;
cursor?: string;
@ -82,8 +91,15 @@ export const agentManagementCreateSchema: z.ZodType<
z.ZodTypeDef,
AgentManagementCreateInput
> = agentCreateSchema.extend({ model: z.string() }).strict();
export const agentManagementUpdateSchema: z.ZodType<AgentManagementUpdate> =
agentUpdateSchema.strict();
export const agentManagementUpdateSchema: z.ZodType<AgentManagementUpdate> = agentUpdateSchema
.extend({
name: z.string().optional(),
description: z.string().optional(),
instructions: z.string().optional(),
model: z.string().optional(),
avatar: z.null().optional(),
})
.strict();
const agentManagementCursorSchema = z
.string()

View file

@ -0,0 +1,232 @@
import { Types } from 'mongoose';
import {
PermissionBits,
Permissions,
PermissionTypes,
ResourceType,
} from 'librechat-data-provider';
import type { IRole, IUser } from '@librechat/data-schemas';
import type { Request, Response } from 'express';
import type { AgentManagementUpdateDeps } from './updates';
import { createAgentManagementUpdateHandler } from './updates';
jest.mock('@librechat/data-schemas', () => {
const actual = jest.requireActual('@librechat/data-schemas');
return {
...actual,
logger: { warn: jest.fn(), error: jest.fn() },
};
});
const user = {
id: new Types.ObjectId().toString(),
tenantId: 'tenant-a',
role: 'USER',
} as IUser;
const objectId = new Types.ObjectId();
const existingAgent = {
_id: objectId,
id: 'agent-existing',
name: 'Existing Agent',
description: 'Keep this description',
provider: 'openAI',
model: 'gpt-5',
version: 1,
createdAt: new Date('2026-09-03T10:00:00.000Z'),
updatedAt: new Date('2026-09-03T10:00:00.000Z'),
};
const updatedAgent = {
...existingAgent,
name: 'Updated Agent',
version: 2,
versions: [{}, {}],
updatedAt: new Date('2026-09-03T11:00:00.000Z'),
};
function makeRequest(overrides: Partial<Request> = {}): Request {
return {
user,
params: { id: existingAgent.id },
body: { name: updatedAgent.name },
...overrides,
} as Request;
}
function makeResponse(): Response {
const response = {} as Response;
response.status = jest.fn(() => response);
response.json = jest.fn(() => response);
return response;
}
function makeDeps(overrides: Partial<AgentManagementUpdateDeps> = {}): AgentManagementUpdateDeps {
return {
getRoleByName: jest.fn().mockResolvedValue({
permissions: {
[PermissionTypes.AGENTS]: {
[Permissions.USE]: true,
[Permissions.CREATE]: true,
},
},
} as IRole),
getAgentWithVersionCount: jest.fn().mockResolvedValue(existingAgent),
checkPermission: jest.fn().mockResolvedValue(true),
hasCapability: jest.fn().mockResolvedValue(false),
updateAgent: jest.fn(async (_req: Request, res: Response) => res.json(updatedAgent)),
...overrides,
};
}
describe('Agent Management update handler', () => {
it('delegates a partial update and returns the management projection', async () => {
const deps = makeDeps();
const request = makeRequest();
const response = makeResponse();
await createAgentManagementUpdateHandler(deps)(request, response);
expect(deps.getAgentWithVersionCount).toHaveBeenCalledWith({
id: existingAgent.id,
tenantId: user.tenantId,
});
expect(deps.checkPermission).toHaveBeenCalledWith({
userId: user.id,
role: user.role,
resourceType: ResourceType.AGENT,
resourceId: objectId,
requiredPermission: PermissionBits.EDIT,
});
expect(deps.updateAgent).toHaveBeenCalledWith(request, expect.anything());
expect(request.body).toEqual({ name: 'Updated Agent' });
expect(response.status).toHaveBeenCalledWith(200);
expect(response.json).toHaveBeenCalledWith(
expect.objectContaining({
id: existingAgent.id,
name: 'Updated Agent',
description: existingAgent.description,
provider: existingAgent.provider,
model: existingAgent.model,
version: 2,
createdAt: '2026-09-03T10:00:00.000Z',
updatedAt: '2026-09-03T11:00:00.000Z',
}),
);
});
it('rejects caller-supplied ownership and tenant fields before updating', async () => {
const deps = makeDeps();
const response = makeResponse();
await createAgentManagementUpdateHandler(deps)(
makeRequest({ body: { name: 'Updated Agent', tenantId: 'tenant-b', author: user.id } }),
response,
);
expect(response.status).toHaveBeenCalledWith(400);
expect(response.json).toHaveBeenCalledWith(
expect.objectContaining({ error: expect.objectContaining({ code: 'invalid_request' }) }),
);
expect(deps.updateAgent).not.toHaveBeenCalled();
});
it('requires the same AGENTS USE and CREATE permissions as browser updates', async () => {
const deps = makeDeps({
getRoleByName: jest.fn().mockResolvedValue({
permissions: {
[PermissionTypes.AGENTS]: {
[Permissions.USE]: true,
[Permissions.CREATE]: false,
},
},
} as never),
});
const response = makeResponse();
await createAgentManagementUpdateHandler(deps)(makeRequest(), response);
expect(response.status).toHaveBeenCalledWith(403);
expect(deps.getAgentWithVersionCount).not.toHaveBeenCalled();
expect(deps.updateAgent).not.toHaveBeenCalled();
});
it('fails closed without a tenant-bound authenticated user', async () => {
const deps = makeDeps();
const response = makeResponse();
await createAgentManagementUpdateHandler(deps)(
makeRequest({ user: { ...user, tenantId: undefined } as IUser }),
response,
);
expect(response.status).toHaveBeenCalledWith(403);
expect(deps.getRoleByName).not.toHaveBeenCalled();
expect(deps.updateAgent).not.toHaveBeenCalled();
});
it('does not disclose an Agent outside the authenticated tenant', async () => {
const deps = makeDeps({ getAgentWithVersionCount: jest.fn().mockResolvedValue(null) });
const response = makeResponse();
await createAgentManagementUpdateHandler(deps)(makeRequest(), response);
expect(response.status).toHaveBeenCalledWith(404);
expect(response.json).toHaveBeenCalledWith({
error: { code: 'not_found', message: 'Agent not found' },
});
expect(deps.checkPermission).not.toHaveBeenCalled();
expect(deps.updateAgent).not.toHaveBeenCalled();
});
it('requires EDIT permission on the tenant-scoped Agent', async () => {
const deps = makeDeps({ checkPermission: jest.fn().mockResolvedValue(false) });
const response = makeResponse();
await createAgentManagementUpdateHandler(deps)(makeRequest(), response);
expect(response.status).toHaveBeenCalledWith(403);
expect(deps.updateAgent).not.toHaveBeenCalled();
});
it('preserves the existing manage-agents capability bypass', async () => {
const deps = makeDeps({
hasCapability: jest.fn().mockResolvedValue(true),
checkPermission: jest.fn().mockResolvedValue(false),
});
const response = makeResponse();
await createAgentManagementUpdateHandler(deps)(makeRequest(), response);
expect(deps.checkPermission).not.toHaveBeenCalled();
expect(deps.updateAgent).toHaveBeenCalled();
});
it('maps shared update conflicts to the stable management error contract', async () => {
const deps = makeDeps({
updateAgent: jest.fn(async (_req: Request, res: Response) =>
res.status(409).json({ error: 'version detail' }),
),
});
const response = makeResponse();
await createAgentManagementUpdateHandler(deps)(makeRequest(), response);
expect(response.status).toHaveBeenCalledWith(400);
expect(response.json).toHaveBeenCalledWith({
error: { code: 'invalid_request', message: 'Invalid request' },
});
});
it('does not expose errors thrown by the shared update flow', async () => {
const deps = makeDeps({
updateAgent: jest.fn().mockRejectedValue(new Error('database connection secret')),
});
const response = makeResponse();
await createAgentManagementUpdateHandler(deps)(makeRequest(), response);
expect(response.status).toHaveBeenCalledWith(500);
expect(response.json).toHaveBeenCalledWith({
error: { code: 'internal_error', message: 'Internal server error' },
});
});
});

View file

@ -0,0 +1,159 @@
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 {
agentManagementUpdateSchema,
mapAgentManagementError,
projectAgentManagementResponse,
} from './management';
import { checkAccessWithRequestCache } from '../middleware/access';
type AgentUpdateHandler = (
req: Request,
res: Response,
) => Promise<Response | void> | Response | void;
type AgentManagementRecord = AgentManagementProjectionSource & { _id: Types.ObjectId };
export interface AgentManagementUpdateDeps {
getRoleByName: (roleName: string, fieldsToSelect?: string | string[]) => Promise<IRole | null>;
getAgentWithVersionCount: (search: {
id: string;
tenantId: string;
}) => Promise<AgentManagementRecord | null>;
checkPermission: (params: {
userId: string;
role?: string;
resourceType: ResourceType;
resourceId: Types.ObjectId;
requiredPermission: PermissionBits;
}) => Promise<boolean>;
hasCapability: (user: IUser, capability: SystemCapability) => Promise<boolean>;
updateAgent: AgentUpdateHandler;
}
function sendError(
res: Response,
code: Parameters<typeof mapAgentManagementError>[0],
error?: unknown,
) {
const mapped = mapAgentManagementError(code, error);
return res.status(mapped.status).json(mapped.body);
}
function mapUpdateStatus(status: number): Parameters<typeof mapAgentManagementError>[0] {
if (status === 400 || status === 409) {
return 'invalid_request';
}
if (status === 401 || status === 403) {
return 'permission_denied';
}
if (status === 404) {
return 'not_found';
}
return 'internal_error';
}
function createResponseAdapter(res: Response): {
response: Response;
getResult: () => Response | undefined;
} {
let statusCode = 200;
let result: Response | undefined;
const adapter = Object.create(res) as Response;
adapter.status = ((status: number) => {
statusCode = status;
return adapter;
}) as Response['status'];
adapter.json = ((body?: AgentManagementProjectionSource) => {
if (statusCode >= 200 && statusCode < 300 && body != null) {
result = res.status(statusCode).json(projectAgentManagementResponse(body));
return result;
}
result = sendError(res, mapUpdateStatus(statusCode));
return result;
}) as Response['json'];
return { response: adapter, getResult: () => result };
}
async function hasManageAgentsCapability(user: IUser, deps: AgentManagementUpdateDeps) {
const capability = ResourceCapabilityMap[ResourceType.AGENT];
try {
return capability != null && (await deps.hasCapability(user, capability));
} catch (error) {
logger.warn(
`[AgentManagement] Agent capability check failed, denying bypass: ${(error as Error).message}`,
);
return false;
}
}
/** Validate and authorize Agent Management updates before reusing the browser update flow. */
export function createAgentManagementUpdateHandler(
deps: AgentManagementUpdateDeps,
): (req: Request, res: Response) => Promise<Response> {
return async function update(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 canUpdate = await checkAccessWithRequestCache({
req,
user,
permissionType: PermissionTypes.AGENTS,
permissions: [Permissions.USE, Permissions.CREATE],
getRoleByName: deps.getRoleByName,
});
if (!canUpdate) {
return sendError(res, 'permission_denied');
}
const agent = await deps.getAgentWithVersionCount({
id: req.params.id,
tenantId: user.tenantId,
});
if (!agent) {
return sendError(res, 'not_found');
}
const canManageAll = await hasManageAgentsCapability(user, deps);
if (
!canManageAll &&
!(await deps.checkPermission({
userId: user.id,
role: user.role,
resourceType: ResourceType.AGENT,
resourceId: agent._id,
requiredPermission: PermissionBits.EDIT,
}))
) {
return sendError(res, 'permission_denied');
}
const parsedBody = agentManagementUpdateSchema.safeParse(req.body);
if (!parsedBody.success) {
return sendError(res, 'invalid_request', parsedBody.error);
}
req.body = parsedBody.data;
const adapter = createResponseAdapter(res);
await deps.updateAgent(req, adapter.response);
return adapter.getResult() ?? sendError(res, 'internal_error');
} catch (error) {
logger.error('[AgentManagement] Error updating Agent', error);
return sendError(res, 'internal_error');
}
};
}