✏️ 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,