fix(api): block takeover while teaching on sibling bot
When a user records a taught skill on one Team Computer bot, manual takeover on another bot in the same space cleared the teaching lease without stopping the session. Teaching input then failed silently and skills/stop could not release the replacement takeover lease. Reject computer/takeover while another bot on the same computer is recording, matching the existing Stop teaching first messaging guard. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
291f23e917
commit
bede7d0d73
@ -172,7 +172,11 @@ import {
|
||||
readServerUpdateStatus,
|
||||
UpdaterProxyError,
|
||||
} from "./server-update.js";
|
||||
import { assertTeachingSendAllowed, createTaughtSkillsService } from "./taught-skills.js";
|
||||
import {
|
||||
assertTeachingSendAllowed,
|
||||
assertTeachingTakeoverAllowed,
|
||||
createTaughtSkillsService,
|
||||
} from "./taught-skills.js";
|
||||
import {
|
||||
isPeerRun,
|
||||
loadAllMessages,
|
||||
@ -1809,6 +1813,12 @@ export function createRouter(deps: RouterDeps) {
|
||||
if (!bot.computer?.providerRef || bot.computer.state !== "running") {
|
||||
throw new ORPCError("BAD_REQUEST", { message: "computer must be running" });
|
||||
}
|
||||
await assertTeachingTakeoverAllowed(
|
||||
deps.prisma,
|
||||
context.actor,
|
||||
input.botId,
|
||||
bot.computer.id,
|
||||
);
|
||||
if (hasActiveComputerControl(bot.computer) && bot.computer.controlBotId === bot.id) {
|
||||
await bindWaitingTakeoverToControl(deps, {
|
||||
spaceId: context.actor.spaceId,
|
||||
|
||||
@ -110,6 +110,27 @@ export async function assertTeachingSendAllowed(
|
||||
}
|
||||
}
|
||||
|
||||
export async function assertTeachingTakeoverAllowed(
|
||||
prisma: PrismaClient,
|
||||
actor: Actor,
|
||||
botId: string,
|
||||
computerId: string,
|
||||
): Promise<void> {
|
||||
const teachingElsewhere = await prisma.taughtSkill.findFirst({
|
||||
where: {
|
||||
spaceId: actor.spaceId,
|
||||
userId: actor.userId,
|
||||
status: "recording",
|
||||
botId: { not: botId },
|
||||
bot: { computerId },
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (teachingElsewhere) {
|
||||
throw new ORPCError("CONFLICT", { message: "Stop teaching first" });
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelActiveRuns(
|
||||
deps: TaughtSkillsDeps,
|
||||
_actor: Actor,
|
||||
|
||||
@ -694,6 +694,46 @@ describeWithDatabase("API authorization and resource isolation", () => {
|
||||
expect(ownerCrossBot.content).toBe(secret);
|
||||
});
|
||||
|
||||
it("blocks Team Computer takeover on another bot while teaching is recording", async () => {
|
||||
const cookie = await signup(app, `teach-takeover-${stamp}@rakazo.test`, "Teach Takeover");
|
||||
const teacher = await rpc<Bot>(app, cookie, "bots/create", botInput("Teacher"));
|
||||
const sibling = await rpc<Bot>(app, cookie, "bots/create", botInput("Sibling"));
|
||||
const [teacherRow, siblingRow] = await Promise.all([
|
||||
handles.prisma.bot.findUniqueOrThrow({ where: { id: teacher.id } }),
|
||||
handles.prisma.bot.findUniqueOrThrow({ where: { id: sibling.id } }),
|
||||
]);
|
||||
expect(teacherRow.computerId).toBe(siblingRow.computerId);
|
||||
|
||||
await rpc(app, cookie, "computer/boot", { botId: teacher.id });
|
||||
const skill = await rpc<{ id: string; status: string }>(app, cookie, "skills/start", {
|
||||
botId: teacher.id,
|
||||
goal: "Demonstrate export",
|
||||
});
|
||||
expect(skill.status).toBe("recording");
|
||||
|
||||
const blockedTakeover = await raw(app, cookie, "computer/takeover", { botId: sibling.id });
|
||||
expect(blockedTakeover.status).toBe(409);
|
||||
expect(await blockedTakeover.text()).toMatch(/Stop teaching first/i);
|
||||
|
||||
await rpc(app, cookie, "computer/input", {
|
||||
botId: teacher.id,
|
||||
kind: "key",
|
||||
payload: { key: "x" },
|
||||
});
|
||||
|
||||
const stopped = await rpc<{ status: string }>(app, cookie, "skills/stop", {
|
||||
skillId: skill.id,
|
||||
});
|
||||
expect(stopped.status).toBe("draft");
|
||||
|
||||
const computer = await handles.prisma.computer.findUniqueOrThrow({
|
||||
where: { id: teacherRow.computerId! },
|
||||
select: { controlHolder: true, controlBotId: true },
|
||||
});
|
||||
expect(computer.controlHolder).toBe("bot");
|
||||
expect(computer.controlBotId).toBeNull();
|
||||
});
|
||||
|
||||
it("scopes Team Computer export to the exporting member's bot workspace in a shared Space", async () => {
|
||||
const owner = await signup(app, `export-owner-${stamp}@rakazo.test`, "Export Owner");
|
||||
const member = await signup(app, `export-member-${stamp}@rakazo.test`, "Export Member");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user