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 <cynarAI@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-09-22 05:25:54 +00:00 committed by Ingo Wagner
parent 33ce657201
commit 4264ef682f
2 changed files with 41 additions and 0 deletions

View File

@ -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<typeof createRepos>);
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",

View File

@ -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 = {