Ctrl K
ComunidadGuías…

server/bot-access.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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);
  });
});