Give workspace computers to attached bots in their own chat

A computer linked to a workspace now shows in the composer (and is usable
by tasks and replies) when chatting with any bot attached to that
workspace, not only in the workspace conversation. Members see those
computers through their assigned workers too, and assign_computer still
describes direct grants only.
This commit is contained in:
2026-10-19 20:03:38 +01:00
parent f57b464e80
commit b0c9c84b2b
6 changed files with 52 additions and 17 deletions
@@ -8,20 +8,28 @@ type Params = {
* workspace's, the default first, and nothing else.
*/
workspace_id?: number | string | null;
/**
* In its own chat, whether the computers of the workspaces it's
* attached to count too (they do by default). The `assign_computer`
* tool's view of direct access turns them off.
*/
include_workspace_computers?: boolean;
};
/**
* Every computer a bot can use where it's working. In a workspace, the
* workspace's computers (none linked: none at all), its default first. In
* its own chat, the ones it was given directly (`bot_computers`); the
* coordinator can use all of them. There the one it works on now
* (`bots.computer_id`) comes first, else the main one, so `computers[0]`
* is where its tasks run by default. The same for every user it works
* for: callers check the user reaches the bot.
* its own chat, the ones it was given directly (`bot_computers`) and those
* of the workspaces it's attached to, since a workspace's computer is
* every bit its own; the coordinator can use all of them. There the one it
* works on now (`bots.computer_id`) comes first, else the main one, so
* `computers[0]` is where its tasks run by default. The same for every
* user it works for: callers check the user reaches the bot.
*/
export default async function grabBotComputers({
bot_id,
workspace_id,
include_workspace_computers = true,
}: Params): Promise<BUN_SQLITE_COBBLE_COMPUTERS[]> {
if (workspace_id) {
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
@@ -39,15 +47,26 @@ export default async function grabBotComputers({
return res.payload || [];
}
const workspace_computers_sql = include_workspace_computers
? `OR c.id IN (
SELECT wc.computer_id FROM workspace_computers wc
JOIN workspaces w ON w.id = wc.workspace_id AND w.archived = 0
JOIN workspace_bots wb ON wb.workspace_id = w.id AND wb.bot_id = ?
)`
: "";
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
sql: `SELECT c.* FROM computers c
WHERE (
(SELECT kind FROM bots WHERE id = ?) = 'coordinator'
OR c.id IN (SELECT computer_id FROM bot_computers WHERE bot_id = ?)
${workspace_computers_sql}
)
ORDER BY c.id = (SELECT computer_id FROM bots WHERE id = ?) DESC,
c.kind = 'main' DESC, c.id ASC`,
values: [bot_id, bot_id, bot_id],
values: include_workspace_computers
? [bot_id, bot_id, bot_id, bot_id]
: [bot_id, bot_id, bot_id],
});
return res.payload || [];
@@ -28,9 +28,10 @@ type Args = {
};
/**
* Lets the coordinator give a crew member access to one of the user's
* computers in its own chat (`bot_computers`), or take it away. In a
* workspace, bots use the workspace's computers instead.
* Lets the coordinator give a crew member direct access to one of the
* user's computers (`bot_computers`), or take it away. The computers of
* the workspaces a bot is attached to are its own too, in its own chat as
* well as in the workspace.
*/
export default function assignComputerTool({
user_id,
@@ -76,7 +77,7 @@ export default function assignComputerTool({
await removeBotFromComputer({ user_id, bot_id: worker.id, computer_id: computer.id });
return `${worker.name} can't use ${ref} in its own chat any more. In a workspace it still uses the workspace's computers.`;
return `${worker.name} can't use ${ref} directly any more. A workspace it's attached to may still give it.`;
},
};
}
+4 -1
View File
@@ -127,7 +127,10 @@ export default async function grabBotChatTools({
await Promise.all(
workers.map(async (worker) => [
worker.id || 0,
await grabBotComputers({ bot_id: worker.id || 0 }),
await grabBotComputers({
bot_id: worker.id || 0,
include_workspace_computers: false,
}),
]),
),
);
+9 -2
View File
@@ -3,7 +3,8 @@ import type { CrewAccessParams, SqlCondition } from "./types";
/**
* The computers a user opens and sees: super admins and admins every one,
* anyone else only those of the bots they chat with (the computers of
* their workspaces, and those their assigned workers were given).
* their workspaces, those their assigned workers were given, and those of
* the workspaces their assigned workers are attached to).
*/
export default function computersSql({ role, user_id, alias = "c" }: CrewAccessParams): SqlCondition {
if (role != "member") return { sql: `1`, values: [] };
@@ -16,7 +17,13 @@ export default function computersSql({ role, user_id, alias = "c" }: CrewAccessP
SELECT bc.computer_id FROM bot_computers bc
JOIN bot_users bu ON bu.bot_id = bc.bot_id AND bu.user_id = ?
JOIN bots b ON b.id = bc.bot_id AND b.kind != 'coordinator'
UNION
SELECT wc.computer_id FROM workspace_computers wc
JOIN workspaces w ON w.id = wc.workspace_id AND w.archived = 0
JOIN workspace_bots wb ON wb.workspace_id = w.id
JOIN bot_users bu ON bu.bot_id = wb.bot_id AND bu.user_id = ?
JOIN bots b ON b.id = wb.bot_id AND b.kind != 'coordinator'
)`,
values: [Number(user_id), Number(user_id)],
values: [Number(user_id), Number(user_id), Number(user_id)],
};
}
@@ -10,13 +10,13 @@ const db = new Database(":memory:");
beforeAll(() => {
db.run(`CREATE TABLE bots (id INTEGER PRIMARY KEY, kind TEXT)`);
db.run(`CREATE TABLE workspaces (id INTEGER PRIMARY KEY)`);
db.run(`CREATE TABLE workspaces (id INTEGER PRIMARY KEY, archived INTEGER DEFAULT 0)`);
db.run(`CREATE TABLE bot_users (bot_id INTEGER, user_id INTEGER)`);
db.run(`CREATE TABLE workspace_users (workspace_id INTEGER, user_id INTEGER)`);
db.run(`CREATE TABLE workspace_bots (workspace_id INTEGER, bot_id INTEGER)`);
db.run(`INSERT INTO bots VALUES (1, 'coordinator'), (2, 'worker'), (3, 'worker')`);
db.run(`INSERT INTO workspaces VALUES (10), (11)`);
db.run(`INSERT INTO bot_users VALUES (2, 7), (1, 7)`);
db.run(`INSERT INTO workspaces VALUES (10, 0), (11, 0)`);
db.run(`INSERT INTO bot_users VALUES (2, 7), (1, 7), (3, 9)`);
db.run(`INSERT INTO workspace_users VALUES (11, 7)`);
db.run(`INSERT INTO workspace_bots VALUES (11, 3)`);
db.run(`CREATE TABLE computers (id INTEGER PRIMARY KEY)`);
@@ -46,6 +46,7 @@ describe("chatBotsSql", () => {
test("a member only with workers assigned to them, even if the coordinator was", () => {
expect(ids({ table: "bots", alias: "b", condition: chatBotsSql({ role: "member", user_id: 7 }) })).toEqual([2]);
expect(ids({ table: "bots", alias: "b", condition: chatBotsSql({ role: "member", user_id: 8 }) })).toEqual([]);
expect(ids({ table: "bots", alias: "b", condition: chatBotsSql({ role: "member", user_id: 9 }) })).toEqual([3]);
});
});
@@ -72,4 +73,8 @@ describe("computersSql", () => {
expect(ids({ table: "computers", alias: "c", condition: computersSql({ role: "member", user_id: 7 }) })).toEqual([21, 22]);
expect(ids({ table: "computers", alias: "c", condition: computersSql({ role: "member", user_id: 8 }) })).toEqual([]);
});
test("a member sees the computers of the workspaces their assigned workers are in", () => {
expect(ids({ table: "computers", alias: "c", condition: computersSql({ role: "member", user_id: 9 }) })).toEqual([21]);
});
});