From 4264ef682f840e8ad72f557a5919d167172d41fb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Sep 2026 05:25:54 +0000 Subject: [PATCH] fix(adapters): scope spawnBot replay to spawning member in shared Spaces spawnKey is unique per Space, not per user. createBot already rejects cross-member replay, but spawnBot caught that error and returned the other member's bot anyway. Match createBot by requiring existing.userId === spawnedBy.userId before treating a spawnKey conflict as idempotent. Co-authored-by: cynarAI --- packages/adapters/src/child-bots.test.ts | 40 ++++++++++++++++++++++++ packages/adapters/src/child-bots.ts | 1 + 2 files changed, 41 insertions(+) diff --git a/packages/adapters/src/child-bots.test.ts b/packages/adapters/src/child-bots.test.ts index cbee9b1..0a04cf4 100644 --- a/packages/adapters/src/child-bots.test.ts +++ b/packages/adapters/src/child-bots.test.ts @@ -41,6 +41,7 @@ describe("spawned bot creation", () => { id: "child-1", name: "Scout", title: "Venue researcher", + userId: "user-1", thread: { id: "thread-1" }, }); const enqueue = vi.fn().mockResolvedValue(undefined); @@ -95,6 +96,45 @@ describe("spawned bot creation", () => { expect(enqueue).toHaveBeenCalledOnce(); }); + it("rejects a spawnKey that belongs to another member in the shared Space", async () => { + const conflict = new Error("unique spawn key"); + const findUnique = vi.fn().mockResolvedValue({ + id: "child-owner", + name: "Scout", + title: "Venue researcher", + userId: "owner-user", + thread: { id: "thread-owner" }, + }); + const createBot = vi.fn().mockRejectedValue(conflict); + const createReposSpy = vi.spyOn(await import("@rakazo/db"), "createRepos").mockReturnValue({ + createBot, + } as unknown as ReturnType); + + await expect( + spawnBot( + { + prisma: { bot: { findUnique } } as unknown as PrismaClient, + jobs: { enqueue: vi.fn() } as unknown as JobPublisher, + }, + { + spawnedBy: { + id: "parent-2", + name: "Other", + spaceId: "workspace-1", + userId: "other-user", + }, + runId: "run-2", + spawnKey: "tool-call-shared", + name: "Scout", + }, + ), + ).rejects.toThrow("unique spawn key"); + + expect(createBot).toHaveBeenCalledOnce(); + expect(findUnique).toHaveBeenCalledOnce(); + createReposSpy.mockRestore(); + }); + it("passes computerMode through to createBot", async () => { const createBot = vi.fn().mockResolvedValue({ id: "child-2", diff --git a/packages/adapters/src/child-bots.ts b/packages/adapters/src/child-bots.ts index dd887a8..08eb6a0 100644 --- a/packages/adapters/src/child-bots.ts +++ b/packages/adapters/src/child-bots.ts @@ -94,6 +94,7 @@ export async function spawnBot( include: { thread: true }, }); if (!existing) throw error; + if (existing.userId !== input.spawnedBy.userId) throw error; if (!existing.thread) throw new Error(`Spawned bot ${existing.id} is missing its thread`); duplicate = true; created = {