diff --git a/backend/__tests__/unit/routes/registry.agent-config-installer-gate.test.js b/backend/__tests__/unit/routes/registry.agent-config-installer-gate.test.js index c33d9d634a..e663edcad2 100644 --- a/backend/__tests__/unit/routes/registry.agent-config-installer-gate.test.js +++ b/backend/__tests__/unit/routes/registry.agent-config-installer-gate.test.js @@ -512,6 +512,30 @@ describe('agent config PATCH — installer gate', () => { expect(AgentProfile.updateMany).not.toHaveBeenCalled(); }); + it('refuses a rename that collides with another installer\'s card in the same pod', async () => { + const primary = installation({ installedBy: MEMBER, displayName: 'Curator' }); + const otherOwnersCard = installation({ + agentName: 'guide', instanceId: 'u1234567890', podId: 'pod-1', + installedBy: INSTALLER, displayName: 'Scout', + }); + setPod(); + AgentInstallation.findOne.mockResolvedValue(primary); + AgentInstallation.find.mockResolvedValueOnce([primary]).mockResolvedValueOnce([primary, otherOwnersCard]); + + const res = await request(app) + .patch('/api/registry/pods/pod-1/agents/openclaw') + .send({ instanceId: 'curator', displayName: 'scout' }); + + expect(res.status).toBe(409); + expect(res.body.code).toBe('agent_display_name_taken'); + expect(AgentIdentityService.renameAgentDisplayName).not.toHaveBeenCalled(); + expect(primary.save).not.toHaveBeenCalled(); + expect(AgentInstallation.find).toHaveBeenLastCalledWith({ + $or: [{ podId: { $in: ['pod-1'] } }, { installedBy: { $in: [MEMBER] } }], + status: 'active', + }); + }); + it('refuses an admin rename that collides with the target installer\'s visible card', async () => { const primary = installation({ installedBy: INSTALLER, displayName: 'Curator' }); const other = installation({ @@ -530,7 +554,8 @@ describe('agent config PATCH — installer gate', () => { expect(res.status).toBe(409); expect(res.body.code).toBe('agent_display_name_taken'); expect(AgentInstallation.find).toHaveBeenLastCalledWith({ - installedBy: { $in: [INSTALLER] }, status: 'active', + $or: [{ podId: { $in: ['pod-1'] } }, { installedBy: { $in: [INSTALLER] } }], + status: 'active', }); expect(primary.displayName).toBe('Curator'); expect(primary.save).not.toHaveBeenCalled(); @@ -561,7 +586,8 @@ describe('agent config PATCH — installer gate', () => { expect(res.status).toBe(409); expect(res.body.code).toBe('agent_display_name_taken'); expect(AgentInstallation.find).toHaveBeenLastCalledWith({ - installedBy: { $in: [INSTALLER, 'other-installer'] }, status: 'active', + $or: [{ podId: { $in: ['pod-1', 'pod-2'] } }, { installedBy: { $in: [INSTALLER, 'other-installer'] } }], + status: 'active', }); expect(primary.save).not.toHaveBeenCalled(); expect(peer.save).not.toHaveBeenCalled(); diff --git a/backend/routes/registry/agent-config.ts b/backend/routes/registry/agent-config.ts index eb6ac5c525..ec5d61249d 100644 --- a/backend/routes/registry/agent-config.ts +++ b/backend/routes/registry/agent-config.ts @@ -349,16 +349,18 @@ agentConfigRouter.patch('/pods/:podId/agents/:name', auth, async (req: any, res: const { status: refusalStatus, ...refusalBody } = writeRefusal; return res.status(refusalStatus).json(refusalBody); } - // An installation can show a different label from its bot User. Guard - // every affected installer's active cards, including when an admin edits - // someone else's agent, before renaming the shared identity. + // An installation can show a different label from its bot User, and a pod + // lists every installer's card side by side. Guard every active card in + // each pod the rename reaches, whoever installed it, plus every card the + // affected installers hold elsewhere, before renaming the shared identity. if (displayName !== undefined) { const requestedName = displayName.trim().toLowerCase(); const installerIds = Array.from(new Set(installationsToUpdate .map((entry: any) => entry.installedBy?.toString?.()) .filter(Boolean))); const ownerInstallations = await AgentInstallation.find({ - installedBy: { $in: installerIds }, status: 'active', + $or: [{ podId: { $in: writablePodIds } }, { installedBy: { $in: installerIds } }], + status: 'active', }); const nameTaken = ownerInstallations.some((entry: any) => ( !(entry.agentName === name.toLowerCase()