Make bot types an extension of the bot's kind
Coordinator and worker come from bots.kind; bot_types now only holds workspace_admin (was admin), each row naming its workspace, with no scope field. Rows from v0.2.8 and earlier are fixed at boot by migrateBotTypes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import type { BotKind } from "@/src/dict/bot-dict";
|
||||
import { BotTypeTargetMatrix } from "@/src/dict/bot-types-dict";
|
||||
import botTypesIn, { type BotTypeRow } from "./bot-types-in";
|
||||
import typeAssignment from "./type-assignment";
|
||||
|
||||
type Params = {
|
||||
actor_kind?: BotKind | "" | null;
|
||||
actor_bot_types?: BotTypeRow[] | null;
|
||||
target_kind?: BotKind | "" | null;
|
||||
target_bot_types?: BotTypeRow[] | null;
|
||||
/**
|
||||
* Where they're working: types count there.
|
||||
@@ -16,9 +19,15 @@ type Params = {
|
||||
* where they're working, through `BotTypeTargetMatrix`: the coordinator
|
||||
* acts on anyone, a workspace admin on the workers there.
|
||||
*/
|
||||
export default function botCanTarget({ actor_bot_types, target_bot_types, workspace_id }: Params): boolean {
|
||||
const actor_types = botTypesIn({ bot_types: actor_bot_types, workspace_id });
|
||||
const target_types = botTypesIn({ bot_types: target_bot_types, workspace_id });
|
||||
export default function botCanTarget({
|
||||
actor_kind,
|
||||
actor_bot_types,
|
||||
target_kind,
|
||||
target_bot_types,
|
||||
workspace_id,
|
||||
}: Params): boolean {
|
||||
const actor_types = botTypesIn({ kind: actor_kind, bot_types: actor_bot_types, workspace_id });
|
||||
const target_types = botTypesIn({ kind: target_kind, bot_types: target_bot_types, workspace_id });
|
||||
|
||||
return typeAssignment({
|
||||
actor_types,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { BotTypePermissionMatrix } from "@/src/dict/bot-types-dict";
|
||||
import type { BotKind } from "@/src/dict/bot-dict";
|
||||
import type { BotPermission } from "@/src/types";
|
||||
import botTypesIn, { type BotTypeRow } from "./bot-types-in";
|
||||
|
||||
type Params = {
|
||||
kind?: BotKind | "" | null;
|
||||
bot_types?: BotTypeRow[] | null;
|
||||
permission: BotPermission;
|
||||
/**
|
||||
@@ -13,12 +15,12 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a bot's types let it do `permission` where it's working
|
||||
* Whether a bot's kind and types let it do `permission` where it's working
|
||||
* (`BotTypePermissionMatrix`). This is the bot's side only: the user the
|
||||
* run works for must still be allowed to do it.
|
||||
*/
|
||||
export default function botCan({ bot_types, permission, workspace_id }: Params): boolean {
|
||||
return botTypesIn({ bot_types, workspace_id }).some((bt) =>
|
||||
export default function botCan({ kind, bot_types, permission, workspace_id }: Params): boolean {
|
||||
return botTypesIn({ kind, bot_types, workspace_id }).some((bt) =>
|
||||
BotTypePermissionMatrix[bt].includes(permission),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BUN_SQLITE_COBBLE_USER_TYPES } from "@/db/types/db";
|
||||
import type { BotKind } from "@/src/dict/bot-dict";
|
||||
import { BotTypeAssignmentMatrix, BotTypeDeletionMatrix } from "@/src/dict/bot-types-dict";
|
||||
import type { BotType } from "@/src/types";
|
||||
import botTypesIn, { type BotTypeRow } from "./bot-types-in";
|
||||
@@ -12,10 +13,13 @@ type Params = {
|
||||
*/
|
||||
actor_user_types?: Pick<BUN_SQLITE_COBBLE_USER_TYPES, "user_type">[] | null;
|
||||
/**
|
||||
* A bot granting it from chat: its types. Only install-wide ones count,
|
||||
* so a workspace admin never grants types.
|
||||
* A bot granting it from chat: its kind and types. Only its kind counts
|
||||
* (no workspace), so a workspace admin never grants types.
|
||||
*/
|
||||
actor_bot_types?: BotTypeRow[] | null;
|
||||
actor_bot?: {
|
||||
kind?: BotKind | "" | null;
|
||||
bot_types?: BotTypeRow[] | null;
|
||||
} | null;
|
||||
target_bot_type: BotType;
|
||||
action: "assign" | "delete";
|
||||
};
|
||||
@@ -27,17 +31,17 @@ type Params = {
|
||||
*/
|
||||
export default function botTypeAssignment({
|
||||
actor_user_types,
|
||||
actor_bot_types,
|
||||
actor_bot,
|
||||
target_bot_type,
|
||||
action,
|
||||
}: Params): boolean {
|
||||
const actor_types = actor_bot_types
|
||||
? botTypesIn({ bot_types: actor_bot_types })
|
||||
const actor_types = actor_bot
|
||||
? botTypesIn({ kind: actor_bot.kind, bot_types: actor_bot.bot_types })
|
||||
: (actor_user_types || []).map((ut) => ut.user_type).filter((ut): ut is string => Boolean(ut));
|
||||
|
||||
return typeAssignment({
|
||||
actor_types,
|
||||
pass_through: actor_bot_types ? false : isPassThroughUser({ user_types: actor_user_types as never }),
|
||||
pass_through: actor_bot ? false : isPassThroughUser({ user_types: actor_user_types as never }),
|
||||
target_type: target_bot_type,
|
||||
action,
|
||||
assignment_matrix: BotTypeAssignmentMatrix,
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOT_TYPES } from "@/db/types/db";
|
||||
import type { BotType } from "@/src/types";
|
||||
import type { BotKind } from "@/src/dict/bot-dict";
|
||||
import type { BotRole } from "@/src/types";
|
||||
|
||||
export type BotTypeRow = Pick<BUN_SQLITE_COBBLE_BOT_TYPES, "bot_type"> & {
|
||||
/**
|
||||
* Null (or empty) for an install-wide type.
|
||||
* The workspace the type holds in.
|
||||
*/
|
||||
workspace_id?: number | "" | null;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The bot's kind (`bots.kind`): a worker when missing.
|
||||
*/
|
||||
kind?: BotKind | "" | null;
|
||||
bot_types?: BotTypeRow[] | null;
|
||||
/**
|
||||
* Where the bot is working: its own chat (null) or a workspace.
|
||||
@@ -20,25 +25,25 @@ type Params = {
|
||||
* Strongest first, so a bot holding several is judged by the one that
|
||||
* reaches furthest.
|
||||
*/
|
||||
const BotTypeRank: BotType[] = ["coordinator", "admin", "worker"];
|
||||
const BotRoleRank: BotRole[] = ["coordinator", "workspace_admin", "worker"];
|
||||
|
||||
/**
|
||||
* The bot types that hold where the bot is working: install-wide rows
|
||||
* everywhere, workspace rows only in their workspace. Strongest first,
|
||||
* `worker` when nothing else holds.
|
||||
* The roles a bot holds where it's working: the coordinator kind
|
||||
* everywhere, a type only in its workspace. Strongest first, `worker` when
|
||||
* nothing else holds.
|
||||
*/
|
||||
export default function botTypesIn({ bot_types, workspace_id }: Params): BotType[] {
|
||||
const held = new Set<BotType>();
|
||||
export default function botTypesIn({ kind, bot_types, workspace_id }: Params): BotRole[] {
|
||||
const held = new Set<BotRole>();
|
||||
|
||||
if (kind == "coordinator") held.add("coordinator");
|
||||
|
||||
for (const row of bot_types || []) {
|
||||
if (!row.bot_type) continue;
|
||||
if (!row.bot_type || !row.workspace_id || !workspace_id) continue;
|
||||
|
||||
const applies = !row.workspace_id || (workspace_id && row.workspace_id == Number(workspace_id));
|
||||
|
||||
if (applies) held.add(row.bot_type);
|
||||
if (row.workspace_id == Number(workspace_id)) held.add(row.bot_type);
|
||||
}
|
||||
|
||||
if (!held.size) held.add("worker");
|
||||
|
||||
return BotTypeRank.filter((bt) => held.has(bt));
|
||||
return BotRoleRank.filter((role) => held.has(role));
|
||||
}
|
||||
|
||||
@@ -4,22 +4,29 @@ import botCanTarget from "./bot-can-target";
|
||||
import botTypeAssignment from "./bot-type-assignment";
|
||||
import botTypesIn from "./bot-types-in";
|
||||
|
||||
const coordinator = [{ bot_type: "coordinator" as const, workspace_id: null }];
|
||||
const admin_of_3 = [{ bot_type: "admin" as const, workspace_id: 3 }];
|
||||
const admin_of_3 = [{ bot_type: "workspace_admin" as const, workspace_id: 3 }];
|
||||
|
||||
describe("botTypesIn", () => {
|
||||
test("a bot with no rows is a worker", () => {
|
||||
expect(botTypesIn({ bot_types: [] })).toEqual(["worker"]);
|
||||
expect(botTypesIn({ kind: "worker", bot_types: [] })).toEqual(["worker"]);
|
||||
});
|
||||
|
||||
test("a workspace type only holds in its workspace", () => {
|
||||
expect(botTypesIn({ bot_types: admin_of_3, workspace_id: 3 })).toEqual(["admin"]);
|
||||
expect(botTypesIn({ bot_types: admin_of_3, workspace_id: 3 })).toEqual(["workspace_admin"]);
|
||||
expect(botTypesIn({ bot_types: admin_of_3, workspace_id: 4 })).toEqual(["worker"]);
|
||||
expect(botTypesIn({ bot_types: admin_of_3 })).toEqual(["worker"]);
|
||||
});
|
||||
|
||||
test("an install-wide type holds everywhere", () => {
|
||||
expect(botTypesIn({ bot_types: coordinator, workspace_id: 9 })).toEqual(["coordinator"]);
|
||||
test("a type row without a workspace holds nowhere", () => {
|
||||
expect(botTypesIn({ bot_types: [{ bot_type: "workspace_admin", workspace_id: null }], workspace_id: 3 })).toEqual([
|
||||
"worker",
|
||||
]);
|
||||
});
|
||||
|
||||
test("the coordinator kind holds everywhere", () => {
|
||||
expect(botTypesIn({ kind: "coordinator", workspace_id: 9 })).toEqual(["coordinator"]);
|
||||
expect(botTypesIn({ kind: "coordinator" })).toEqual(["coordinator"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,12 +42,12 @@ describe("botCan", () => {
|
||||
});
|
||||
|
||||
test("the coordinator can do everything", () => {
|
||||
expect(botCan({ bot_types: coordinator, permission: "manage_extensions" })).toBe(true);
|
||||
expect(botCan({ bot_types: coordinator, permission: "delegate_task", workspace_id: 3 })).toBe(true);
|
||||
expect(botCan({ kind: "coordinator", permission: "manage_extensions" })).toBe(true);
|
||||
expect(botCan({ kind: "coordinator", permission: "delegate_task", workspace_id: 3 })).toBe(true);
|
||||
});
|
||||
|
||||
test("a worker can't delegate", () => {
|
||||
expect(botCan({ bot_types: [], permission: "delegate_task", workspace_id: 3 })).toBe(false);
|
||||
expect(botCan({ kind: "worker", bot_types: [], permission: "delegate_task", workspace_id: 3 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,22 +58,22 @@ describe("botCanTarget", () => {
|
||||
|
||||
test("a workspace admin doesn't direct another admin or the coordinator", () => {
|
||||
expect(botCanTarget({ actor_bot_types: admin_of_3, target_bot_types: admin_of_3, workspace_id: 3 })).toBe(false);
|
||||
expect(botCanTarget({ actor_bot_types: admin_of_3, target_bot_types: coordinator, workspace_id: 3 })).toBe(false);
|
||||
expect(botCanTarget({ actor_bot_types: admin_of_3, target_kind: "coordinator", workspace_id: 3 })).toBe(false);
|
||||
});
|
||||
|
||||
test("an admin elsewhere is a worker here", () => {
|
||||
expect(botCanTarget({ actor_bot_types: admin_of_3, target_bot_types: [], workspace_id: 4 })).toBe(false);
|
||||
expect(botCanTarget({
|
||||
actor_bot_types: admin_of_3,
|
||||
target_bot_types: [{ bot_type: "admin", workspace_id: 8 }],
|
||||
target_bot_types: [{ bot_type: "workspace_admin", workspace_id: 8 }],
|
||||
workspace_id: 3,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
test("the coordinator directs workers and admins, never itself", () => {
|
||||
expect(botCanTarget({ actor_bot_types: coordinator, target_bot_types: [], workspace_id: 3 })).toBe(true);
|
||||
expect(botCanTarget({ actor_bot_types: coordinator, target_bot_types: admin_of_3, workspace_id: 3 })).toBe(true);
|
||||
expect(botCanTarget({ actor_bot_types: coordinator, target_bot_types: coordinator })).toBe(false);
|
||||
expect(botCanTarget({ actor_kind: "coordinator", target_bot_types: [], workspace_id: 3 })).toBe(true);
|
||||
expect(botCanTarget({ actor_kind: "coordinator", target_bot_types: admin_of_3, workspace_id: 3 })).toBe(true);
|
||||
expect(botCanTarget({ actor_kind: "coordinator", target_kind: "coordinator" })).toBe(false);
|
||||
});
|
||||
|
||||
test("a worker directs no one", () => {
|
||||
@@ -79,19 +86,14 @@ describe("botTypeAssignment", () => {
|
||||
|
||||
test("super admins and admins grant and remove workspace admin", () => {
|
||||
for (const action of ["assign", "delete"] as const) {
|
||||
expect(botTypeAssignment({ actor_user_types: user("super_admin"), target_bot_type: "admin", action })).toBe(true);
|
||||
expect(botTypeAssignment({ actor_user_types: user("admin"), target_bot_type: "admin", action })).toBe(true);
|
||||
expect(botTypeAssignment({ actor_user_types: user("member"), target_bot_type: "admin", action })).toBe(false);
|
||||
expect(botTypeAssignment({ actor_user_types: user("super_admin"), target_bot_type: "workspace_admin", action })).toBe(true);
|
||||
expect(botTypeAssignment({ actor_user_types: user("admin"), target_bot_type: "workspace_admin", action })).toBe(true);
|
||||
expect(botTypeAssignment({ actor_user_types: user("member"), target_bot_type: "workspace_admin", action })).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("only the super admin passes for the coordinator type", () => {
|
||||
expect(botTypeAssignment({ actor_user_types: user("admin"), target_bot_type: "coordinator", action: "assign" })).toBe(false);
|
||||
});
|
||||
|
||||
test("the coordinator bot grants workspace admin, a workspace admin doesn't", () => {
|
||||
expect(botTypeAssignment({ actor_bot_types: coordinator, target_bot_type: "admin", action: "assign" })).toBe(true);
|
||||
expect(botTypeAssignment({ actor_bot_types: admin_of_3, target_bot_type: "admin", action: "assign" })).toBe(false);
|
||||
expect(botTypeAssignment({ actor_bot_types: coordinator, target_bot_type: "coordinator", action: "assign" })).toBe(false);
|
||||
expect(botTypeAssignment({ actor_bot: { kind: "coordinator" }, target_bot_type: "workspace_admin", action: "assign" })).toBe(true);
|
||||
expect(botTypeAssignment({ actor_bot: { kind: "worker", bot_types: admin_of_3 }, target_bot_type: "workspace_admin", action: "assign" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user