mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
Prevent agents from mutating budgets
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
9c6f551595
commit
a839769f55
2 files changed
+94
-14
No files matched your search
@@ -147,6 +147,13 @@ beforeEach(() => {
|
||||
budgetMonthlyCents: 100,
|
||||
spentMonthlyCents: 0,
|
||||
});
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
name: "Budget Agent",
|
||||
budgetMonthlyCents: 100,
|
||||
spentMonthlyCents: 0,
|
||||
});
|
||||
mockAgentService.update.mockResolvedValue({
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
@@ -216,13 +223,6 @@ describe("cost routes", () => {
|
||||
});
|
||||
|
||||
it("rejects agent budget updates for board users outside the agent company", async () => {
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
name: "Budget Agent",
|
||||
budgetMonthlyCents: 100,
|
||||
spentMonthlyCents: 0,
|
||||
});
|
||||
const app = await createAppWithActor({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
@@ -238,6 +238,92 @@ describe("cost routes", () => {
|
||||
expect(res.status).toBe(403);
|
||||
expect(mockAgentService.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects agent budget updates from the target agent without changing the budget policy", async () => {
|
||||
const app = await createAppWithActor({
|
||||
type: "agent",
|
||||
agentId: "agent-1",
|
||||
companyId: "company-1",
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/agents/agent-1/budgets")
|
||||
.send({ budgetMonthlyCents: 2500 });
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body).toEqual({ error: "Board access required" });
|
||||
expect(mockAgentService.update).not.toHaveBeenCalled();
|
||||
expect(mockBudgetService.upsertPolicy).not.toHaveBeenCalled();
|
||||
expect(mockLogActivity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects agent budget updates from another same-company agent without changing the budget policy", async () => {
|
||||
const app = await createAppWithActor({
|
||||
type: "agent",
|
||||
agentId: "agent-2",
|
||||
companyId: "company-1",
|
||||
runId: "run-2",
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/agents/agent-1/budgets")
|
||||
.send({ budgetMonthlyCents: 2500 });
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body).toEqual({ error: "Board access required" });
|
||||
expect(mockAgentService.update).not.toHaveBeenCalled();
|
||||
expect(mockBudgetService.upsertPolicy).not.toHaveBeenCalled();
|
||||
expect(mockLogActivity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows authorized board users to update an agent budget and budget policy", async () => {
|
||||
mockAgentService.update.mockResolvedValueOnce({
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
name: "Budget Agent",
|
||||
budgetMonthlyCents: 2500,
|
||||
spentMonthlyCents: 0,
|
||||
});
|
||||
const app = await createAppWithActor({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "session",
|
||||
isInstanceAdmin: false,
|
||||
companyIds: ["company-1"],
|
||||
memberships: [{ companyId: "company-1", status: "active", membershipRole: "admin" }],
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch("/api/agents/agent-1/budgets")
|
||||
.send({ budgetMonthlyCents: 2500 });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(mockAgentService.update).toHaveBeenCalledWith("agent-1", { budgetMonthlyCents: 2500 });
|
||||
expect(mockBudgetService.upsertPolicy).toHaveBeenCalledWith(
|
||||
"company-1",
|
||||
{
|
||||
scopeType: "agent",
|
||||
scopeId: "agent-1",
|
||||
amount: 2500,
|
||||
windowKind: "calendar_month_utc",
|
||||
},
|
||||
"board-user",
|
||||
);
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
companyId: "company-1",
|
||||
actorType: "user",
|
||||
actorId: "board-user",
|
||||
agentId: null,
|
||||
action: "agent.budget_updated",
|
||||
entityType: "agent",
|
||||
entityId: "agent-1",
|
||||
details: { budgetMonthlyCents: 2500 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||
|
||||
@@ -290,13 +290,7 @@ export function costRoutes(db: Db) {
|
||||
}
|
||||
|
||||
assertCompanyAccess(req, agent.companyId);
|
||||
|
||||
if (req.actor.type === "agent") {
|
||||
if (req.actor.agentId !== agentId) {
|
||||
res.status(403).json({ error: "Agent can only change its own budget" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
assertBoard(req);
|
||||
|
||||
const updated = await agents.update(agentId, { budgetMonthlyCents: req.body.budgetMonthlyCents });
|
||||
if (!updated) {
|
||||
|
||||
Reference in new issue
Block a user