From 3f9c1a0771225b7346612dae201a308534cc6005 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 12 Apr 2026 14:54:17 -0500 Subject: [PATCH] fix: reject cross-binding memory forget requests --- server/src/__tests__/memory-service.test.ts | 47 +++++++++++++++++++++ server/src/services/memory.ts | 4 ++ 2 files changed, 51 insertions(+) create mode 100644 server/src/__tests__/memory-service.test.ts diff --git a/server/src/__tests__/memory-service.test.ts b/server/src/__tests__/memory-service.test.ts new file mode 100644 index 00000000000..319989deba5 --- /dev/null +++ b/server/src/__tests__/memory-service.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it, vi } from "vitest"; +import { memoryService } from "../services/memory.js"; + +describe("memoryService.forget", () => { + it("rejects record sets that span multiple bindings", async () => { + const rows = [ + { + id: "11111111-1111-4111-8111-111111111111", + companyId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + bindingId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + }, + { + id: "22222222-2222-4222-8222-222222222222", + companyId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + bindingId: "cccccccc-cccc-4ccc-8ccc-cccccccccccc", + }, + ]; + + const where = vi.fn().mockResolvedValue(rows); + const db = { + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnValue({ where }), + }), + update: vi.fn(), + } as any; + + await expect( + memoryService(db).forget( + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + { + recordIds: rows.map((row) => row.id), + scope: {}, + }, + { + actorType: "user", + actorId: "board-user", + agentId: null, + userId: "board-user", + runId: null, + }, + ), + ).rejects.toThrow("Memory records must belong to the same binding"); + + expect(where).toHaveBeenCalledOnce(); + expect(db.update).not.toHaveBeenCalled(); + }); +}); diff --git a/server/src/services/memory.ts b/server/src/services/memory.ts index b1c8fba80ca..4735eef2a3a 100644 --- a/server/src/services/memory.ts +++ b/server/src/services/memory.ts @@ -787,6 +787,10 @@ export function memoryService(db: Db) { if (recordIds.length === 0) { throw notFound("Memory records not found"); } + const bindingIds = new Set(rows.map((row) => row.bindingId)); + if (bindingIds.size > 1) { + throw unprocessable("Memory records must belong to the same binding"); + } const binding = await getBindingOrThrow(rows[0].bindingId); await db .update(memoryLocalRecords)