server/bot-access.test.ts
Rama main · réplica de lectura del repositorio original.
import type { IncomingMessage } from "node:http";
import { PassThrough } from "node:stream";
import { describe, expect, it } from "vitest";
import { accessFromVisibility, atLeast, sharedLevel, visibilityFromAccess } from "../shared/bot-access.ts";
import { accessOf, memberBot, memberLevel, parseBotAccess, VisibleSet } from "./bot-visibility.ts";
import { readBody } from "./harness/http.ts";
import { createBotAccessGate, levelNeeded, relayedSendRefusal, setupCardRefusal } from "./host/bot-access-gate.ts";
import type { RequestAuth } from "./request-auth.ts";
import { memberBotPatchViolation } from "./request-auth.ts";
import { settledAccess } from "./routes/bot-access.ts";
import type { BotRecord } from "./store.ts";
import type { BotVisibility } from "../shared/wire.ts";
const BOB = "bob@acme.test", CARA = "cara@acme.test", DAN = "dan@acme.test";
describe("moving visibility to levels", () => {
it("gives what each older setting gave: everyone chats, listed people chat, admins only stays admins only", () => {
expect(accessFromVisibility(undefined)).toEqual({ everyone: "chat", people: {} });
expect(accessFromVisibility("everyone")).toEqual({ everyone: "chat", people: {} });
expect(accessFromVisibility("admins")).toEqual({ everyone: "none", people: {} });
expect(accessFromVisibility({ people: [BOB, "@acme.test"] })).toEqual({ everyone: "none", people: { [BOB]: "chat", "@acme.test": "chat" } });
});
it("keeps everyone a member could see able to see it, and nobody else", () => {
for (const visibility of ["everyone", "admins", { people: [BOB] }] satisfies BotVisibility[]) {
expect(visibilityFromAccess(accessFromVisibility(visibility), null)).toEqual(visibility);
}
expect(visibilityFromAccess({ everyone: "view", people: {} }, null)).toBe("everyone");
expect(visibilityFromAccess({ everyone: "none", people: {} }, DAN)).toEqual({ people: [DAN] });
});
});
describe("a member's level", () => {
const bot = { visibility: { people: [BOB, CARA, DAN] }, access: { everyone: "none", people: { [BOB]: "view", [CARA]: "chat", [DAN]: "edit" } } };
it("is the higher of everyone's and theirs, never below viewing a bot they see, and none for one they don't", () => {
expect(memberLevel(bot, BOB)).toBe("view");
expect(memberLevel(bot, CARA)).toBe("chat");
expect(memberLevel(bot, DAN)).toBe("edit");
expect(memberLevel(bot, "eve@acme.test")).toBe("none");
expect(memberLevel({ ...bot, visibility: "everyone", access: { everyone: "chat", people: { [BOB]: "view" } } }, BOB)).toBe("chat");
expect(sharedLevel({ everyone: "none", people: { "@acme.test": "view" } }, "zed@acme.test")).toBe("view");
});
it("is edit for the bot's owner, and chat for anyone a bot without sharing shows itself to", () => {
expect(memberLevel({ visibility: { people: [BOB] }, access: { everyone: "none", people: {} }, reportsTo: BOB }, BOB)).toBe("edit");
expect(memberLevel({ visibility: { people: [BOB] }, defaultBot: { person: BOB } }, BOB)).toBe("edit");
expect(memberLevel({ visibility: "everyone" }, CARA)).toBe("chat");
expect(memberLevel({ visibility: { people: [BOB] } }, BOB)).toBe("chat");
});
it("fails closed to viewing when the stored sharing is damaged", () => {
expect(accessOf({ access: { everyone: "root" } })).toEqual({ everyone: "none", people: {} });
expect(memberLevel({ visibility: "everyone", access: "edit" }, BOB)).toBe("view");
});
it("reaches each copy of the bot: an editor gets its sharing, nobody else does", () => {
const bots = [{ id: "pay", threadId: "t-pay", ...bot }];
const audiences = { of: () => undefined, person: null };
const as = (email: string) => memberBot({ id: "pay", threadId: "t-pay", ...bot }, new VisibleSet(bots, [], { kind: "member", email }, undefined, audiences));
expect(as(BOB)).toMatchObject({ yourAccess: "view" });
expect((as(BOB) as { access?: unknown }).access).toBeUndefined();
expect((as(DAN) as { access?: unknown }).access).toEqual(bot.access);
const admin = memberBot({ id: "pay", threadId: "t-pay", ...bot }, new VisibleSet(bots, [], { kind: "all", email: "ada@acme.test" }, undefined, audiences));
expect(admin).toMatchObject({ yourAccess: "edit", access: bot.access });
// Off a team a member keeps chatting with every bot they see, and no copy carries a level.
const offTeam = new VisibleSet([{ id: "pay", threadId: "t-pay", visibility: "everyone" }], [], { kind: "member", email: BOB });
expect(offTeam.level("pay")).toBe("chat");
expect((memberBot({ id: "pay", threadId: "t-pay" }, offTeam) as { yourAccess?: unknown }).yourAccess).toBeUndefined();
});
});
describe("what a request needs", () => {
it("reads at view, writes at chat, sharing at edit, and leaves a bot's own PATCH and routines to their routes", () => {
expect(levelNeeded("GET", "/api/threads/t1/messages")).toBe("view");
expect(levelNeeded("POST", "/api/bots/b1/read")).toBe("view");
expect(levelNeeded("POST", "/api/bots/b1/bot-api")).toBe("edit");
expect(levelNeeded("POST", "/api/bots/b1/bot-api/new-key")).toBe("edit");
expect(levelNeeded("GET", "/api/bots/b1/bot-api")).toBe("view");
expect(levelNeeded("POST", "/api/groups/g1/read")).toBe("view");
expect(levelNeeded("POST", "/api/bots/b1/tasks/t1")).toBe("view");
expect(levelNeeded("POST", "/api/bots/b1/tasks/delete")).toBe("chat");
expect(levelNeeded("POST", "/api/bots/b1/tasks")).toBe("chat");
expect(levelNeeded("PATCH", "/api/bots/b1/tasks/t1")).toBe("chat");
expect(levelNeeded("POST", "/api/bots/b1/messages")).toBe("chat");
expect(levelNeeded("POST", "/api/threads/t1/respond")).toBe("chat");
expect(levelNeeded("PATCH", "/api/bots/b1/profile")).toBe("chat");
expect(levelNeeded("PUT", "/api/bots/b1/access")).toBe("edit");
expect(levelNeeded("PATCH", "/api/bots/b1")).toBeNull();
expect(levelNeeded("DELETE", "/api/routines/r1")).toBeNull();
});
it("lets each level change only its own fields of a bot", () => {
expect(memberBotPatchViolation({ unread: false }, "view")).toBeNull();
expect(memberBotPatchViolation({ color: "teal" }, "view")).toBe("color");
expect(memberBotPatchViolation({ color: "teal" }, "chat")).toBeNull();
// A Team Cloud member who may only chat changes no other bot's look; one who may edit it can.
expect(memberBotPatchViolation({ color: "teal" }, "chat", false, true)).toBe("color");
expect(memberBotPatchViolation({ color: "teal", name: "Mine" }, "chat", true, true)).toBeNull();
expect(memberBotPatchViolation({ color: "teal" }, "edit", false, true)).toBeNull();
expect(memberBotPatchViolation({ soul: "x" }, "chat")).toBe("soul");
expect(memberBotPatchViolation({ soul: "x", modelSelection: {}, threadId: "t", composio: true, connectorTools: {}, mcpServers: [] }, "edit")).toBeNull();
for (const field of ["approvalMode", "autoApprove", "alwaysAllow", "cwd", "computer", "section", "chiefOfStaff", "peers", "visibility", "access", "reportsTo", "browserProfile"]) {
expect(memberBotPatchViolation({ [field]: null }, "edit"), field).toBe(field);
}
});
it("asks someone who can edit the bot to approve a change to it, and anyone who may answer for anything else", () => {
const bot = { id: "pay", name: "Payroll" };
const level = (who: string) => () => who as "view" | "chat" | "edit";
expect(setupCardRefusal({ profileRequest: {} }, bot, level("chat"))).toMatch(/edit Payroll/);
expect(setupCardRefusal({ modelRequest: {} }, bot, level("chat"))).toMatch(/edit/);
expect(setupCardRefusal({ profileRequest: {} }, bot, level("edit"))).toBeNull();
expect(setupCardRefusal({}, bot, level("chat"))).toBeNull();
});
it("keeps a view-only refusal's key and bot name on both path and parsed-body checks", async () => {
const bot = { id: "pay", threadId: "t-pay", visibility: { people: [BOB] }, access: { everyone: "none" as const, people: { [BOB]: "view" as const } } };
const visible = new VisibleSet([bot], [], { kind: "member", email: BOB }, undefined, { of: () => undefined, person: null });
const auth: RequestAuth = {
kind: "session", via: "bearer", scopes: ["client"],
session: { id: "bob", email: BOB, tokenHash: "0".repeat(64), label: "Bob", scopes: ["client"], createdAt: 0, lastSeenAt: 0, expiresAt: 1 },
};
const gate = createBotAccessGate({
botName: () => "Payroll", roomBots: () => undefined, threadOwner: () => ({ bot: "pay" }),
routine: () => undefined, run: () => undefined, routineWriter: () => undefined,
});
const stream = new PassThrough();
const req = stream as unknown as IncomingMessage;
const refusal = { error: "You can view Payroll, not chat with it.", errorKey: "serverError.bot.viewOnly", errorParams: { bot: "Payroll" } };
expect(gate(req, auth, "POST", "/api/bots/pay/messages", visible)).toEqual(refusal);
expect(gate(req, auth, "POST", "/api/groups", visible)).toBeNull();
const body = readBody(req);
stream.end(JSON.stringify({ memberIds: ["pay"] }));
await expect(body).rejects.toMatchObject({ message: refusal.error, status: 403, errorKey: refusal.errorKey, errorParams: refusal.errorParams });
});
it("relays another app's message only for someone who can chat with the bot", () => {
const bot = { name: "Payroll", visibility: { people: [BOB, CARA] }, access: { everyone: "none", people: { [BOB]: "view", [CARA]: "chat" } } };
expect(relayedSendRefusal(bot, BOB, false)).toMatch(/can't chat/);
expect(relayedSendRefusal(bot, undefined, false)).toMatch(/can't chat/);
expect(relayedSendRefusal(bot, CARA, false)).toBeNull();
expect(relayedSendRefusal(bot, "ada@acme.test", true)).toBeNull();
expect(relayedSendRefusal({ name: "Open", visibility: "everyone" }, undefined, false)).toBeNull();
});
});
describe("setting a bot's sharing", () => {
const bot = { id: "pay", name: "Payroll", reportsTo: DAN, access: { everyone: "none", people: { "@old.test": "chat" } } } as unknown as BotRecord;
const member = (email: string) => [BOB, CARA].includes(email);
it("adds only the team's people, keeps an entry the bot already had, and never lists the owner", () => {
expect(settledAccess(bot, { everyone: "view", people: { [BOB]: "edit", "@old.test": "view", [DAN]: "view" } }, member))
.toEqual({ ok: true, access: { everyone: "view", people: { [BOB]: "edit", "@old.test": "view" } } });
expect(settledAccess(bot, { everyone: "none", people: { "stranger@else.test": "chat" } }, member)).toMatchObject({ ok: false });
});
it("takes only well-formed levels and addresses", () => {
expect(parseBotAccess({ everyone: "chat", people: { " Bob@Acme.test ": "edit" } })).toEqual({ ok: true, access: { everyone: "chat", people: { [BOB]: "edit" } } });
expect(parseBotAccess({ everyone: "admin", people: {} }).ok).toBe(false);
expect(parseBotAccess({ everyone: "chat", people: { [BOB]: "none" } }).ok).toBe(false);
expect(parseBotAccess({ everyone: "chat", people: { "not an address": "view" } }).ok).toBe(false);
expect(parseBotAccess({ everyone: "chat", people: {}, extra: 1 }).ok).toBe(false);
expect(atLeast("edit", "chat")).toBe(true);
expect(atLeast("view", "chat")).toBe(false);
});
});