Add bot types with a permission matrix, and workspace admin bots

A bot's powers came from bots.kind alone, so only the coordinator could
hand work to other bots. bot_types now works like user_types: a row with
no workspace holds everywhere (the coordinator), one with a workspace
only there (a workspace admin), and no row is a worker. bots.kind stays
the legacy marker of the one coordinator for who sees it.

The matrices in src/dict/bot-types-dict.ts say what each type may do,
whom it may direct and who grants it, read by one generic interpreter
(rbac/type-assignment.ts) that userTypeAssignment now wraps too. Every
run reads its powers once (grabBotRunPowers) and the chat tools, task
runs and task tools gate on them instead of kind; a run's powers are its
type's and its user's together.

In the workspace it leads, an admin hands tasks to the workers there
(results relayed back to it), sets their instructions, adds tasks to
their lists and keeps the workspace's context when its user may. Super
admins and admins grant it from the workspace's settings or the chat
header's menu, and the coordinator with set_bot_type. Admins are marked
with a filled green shield beside their name across the workspace, kept
live by pushBotUpdated's admin_workspace_ids.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-25 09:04:14 +01:00
co-authored by Claude Opus 5.5
parent 4161863587
commit ea62dd11f0
65 changed files with 1970 additions and 239 deletions
+2 -1
View File
@@ -97,7 +97,8 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - a read-onl
### Bots (the chat shell)
- **Access (the crew is shared):** bots, workspaces and computers belong to the install, not to a user (their `user_id` is only who created them, not a foreign key). Nobody reaches a bot until a super admin or admin grants it: `bot_users` (a worker assigned to a user, for its own chat) and `workspace_users` (a user added to a workspace, to chat with every worker in it there). `crewRole` (`src/utils/rbac/crew-role.ts`): `super_admin` reaches everything and is the only one who ever sees, chats with or edits the coordinator; `admin` reaches every worker and workspace; anyone else (`member`) only their grants. `canManageCrew` (super admins and admins) creates, edits and deletes workers and workspaces, sets their models and pictures, and hands out access (the Bots and Workspaces groups on a user's settings page, `user-access-groups.tsx`, each added or taken away at once by `api/admin/users/add-access` and `remove-access` through `setUserGrant`); the structural DB helpers check it themselves (`assertCrewManager`, `src/functions/backend/db/access/`). The SQL conditions live in `src/utils/crew-access/` (`chatBotsSql`, `workspacesSql`, `workspaceBotsSql`, `computersSql`, tested against SQLite) and back `grabChatBot` (its own chat), `grabUserBot` (anywhere: its own chat or a workspace), `grabUserWorkspace(s)`, `grabWorkspaceBot`, `grabBots` and `grabUserComputer(s)`. Conversations are per user (`bot_sessions.user_id`): a job runs for the user whose session it's in (`runBotJob` reads it), with that user's memory and only what they reach. A bot's face and status line are per user too (`bot_user_faces`, set by `setBotFace` for the job's user, `grabJobUserId`; no row is idle), so needs-you or working only shows to the user the job is for; every bot read for a page overlays the viewer's (`botFaceSql`, `src/utils/bot-face-sql.ts`). Live updates go to the session's user (`pushToSessionUser`), a face change to its user, and a bot change (its computer) to everyone who sees it (`pushBotUpdated`, `grabBotAudience`, each with their own face and active task); its status line never names a task (`BotTaskStatusLine`). Bot and workspace pictures are served to whoever sees them (`grabSharedPictureMedia`). A member with nothing granted lands on an empty `/admin/bots`; a workspace's dashboard is open to everyone in it, its settings link and computers' settings links only to `canManageCrew`. `src/scripts/share-crew.ts` (safe to rerun) moved an install from one crew per user to this.
- **Tables:** `bots` (`kind` worker|coordinator, `color` (a free `#rrggbb` the user picks) and `stone` (which flat-cut shape it's drawn on), `role`, `system_prompt` (the bot's core instructions, markdown); `hue` is the palette column they replaced, read only as a fallback), `bot_user_faces` (`face`, `status_line` per bot and user), `bot_sessions`, `bot_messages`, `bot_tasks`, `bot_task_steps`. Enums in `src/dict/bot-dict.ts`.
- **Bot types (what a bot can do):** `bot_types` is to bots what `user_types` is to users (`bot_id`, `bot_type`, `workspace_id` null for install-wide, `granted_by_user_id`/`granted_by_bot_id`; one per bot, type and workspace, enforced in `setBotType`). Types live in `src/dict/bot-types-dict.ts`: `coordinator` (install-wide, written only by `ensureCoordinatorBot` through `ensureCoordinatorType`, which also backfills older installs; `src/scripts/backfill-bot-types.ts` for a headless one), `admin` (a workspace admin, scoped to one workspace) and `worker` (no row). `BotTypePermissionMatrix` says what each may do (`BotPermissions`: a workspace admin gets `delegate_task`, `set_member_instructions`, `manage_workspace_tasks`, `edit_workspace` in its workspace; the coordinator everything), `BotTypeTargetMatrix` whom it may direct (an admin directs that workspace's workers, never another admin or the coordinator), and `BotTypeAssignmentMatrix`/`BotTypeDeletionMatrix` who grants them (super admins and admins, and the coordinator bot through `set_bot_type`; both the bot and the user it runs for must pass). All three are read by the generic interpreter `src/utils/rbac/type-assignment.ts`, which `userTypeAssignment` wraps too; `botTypesIn`, `botCan`, `botCanTarget` and `botTypeAssignment` sit beside it (`bot-types.test.ts`). Helpers in `src/functions/backend/db/bot-types/` (`grabBotTypes`, which also counts the legacy `kind = 'coordinator'` bot as the coordinator, `grabBotsTypes`, `grabWorkspaceAdmins`, `grabDirectableBots`, `setBotType`/`removeBotType` checked by `assertBotTypeChange`: only workspace types, only for a bot in that workspace) and `grabBotRunPowers`, which every run reads once: `run-bot-reply`, `run-bot-task` and `grabBotChatTools` gate on its `is_coordinator`, `admin_workspace_ids` and `can(permission)`, never on `kind`. A run's powers are its type's and its user's together: an admin's `update_workspace` only shows when the user is a crew manager. In the workspace it leads an admin gets `grabWorkspaceAdminTools` (`delegate_task` for the members it may direct, results relayed back to it like the coordinator's, `set_bot_workspace_instructions`, `update_workspace` for that workspace only), `create_task` onto members' own lists and the tasks it put there (`botTaskScopesSql`'s `admin_workspace_ids`), and `workspaceAdminNote`; the other bots there are told who leads them (`workspaceAdminsNote`). `bots.kind` stays the legacy marker of the one coordinator for who sees it (the crew-access SQL, rail, audience). A crew manager makes a bot admin from the workspace's settings (`workspace-bot-row.tsx`'s shield toggle) or from the bot's chat in that workspace (the header menu's "Make workspace admin" row, `bot-admin-menu-item.tsx`, its state kept by `ConversationHeader` from the live bot), both through `api/admin/bots/set-bot-type`, which pushes `pushBotUpdated` (its `admin_workspace_ids`, applied by `useLiveBots`). In a workspace every crew row carries `is_admin` (`grabBots`; `BotWorkspace.is_admin` on a bot's settings), and an admin is marked with `AdminShield` (`src/components/general/admin-shield.tsx`: a filled success-green shield, beside the name and never on the stone) in the rail, the chat header, `[bot:ID]` chips, the workspace dashboard and both settings lists; the menu row and the settings toggle fill green too. Removing a bot from a workspace, or deleting either, drops its types there.
- **Tables:** `bots` (`kind` worker|coordinator, legacy for access, see Bot types, `color` (a free `#rrggbb` the user picks) and `stone` (which flat-cut shape it's drawn on), `role`, `system_prompt` (the bot's core instructions, markdown); `hue` is the palette column they replaced, read only as a fallback), `bot_user_faces` (`face`, `status_line` per bot and user), `bot_sessions`, `bot_messages`, `bot_tasks`, `bot_task_steps`. Enums in `src/dict/bot-dict.ts`.
- **Threads and sessions:** each user has one conversation with a bot in its own chat and one per workspace it's in (the coordinator in every workspace). Each is split into `bot_sessions` (`user_id`, `bot_id`, `workspace_id` null for its own chat, `status` active|closed, `title` and `summary` written by the bot from the first exchange, `summary_message_count` (how many messages the summary covers), `previous_session_id`, `unread`, `last_activity_at`); `bot_messages`, `bot_tasks` and `bots_queue` carry `session_id` and `workspace_id`. Helpers in `src/functions/backend/db/bot-sessions/`: `ensureBotSession` (the active one, else a new one; `rollover` starts the next once it has `AppData.BotSessionMaxMessages`), `startBotSession`, `reopenBotSession`, `grabBotSessions` (search over title, summary and message text), `grabUserConversation` (access and membership), `grabConversationProps` (the conversation pages' props). Once a session's first exchange is in, `name-bot-session.ts` has the bot name it (the title the rail and list show) and write its summary, after the reply is pushed; a new session's first reply gets the previous one's summary (`grab-session-carry-over.ts`), rewritten first when it covers fewer messages than that session ended with. The header's sessions icon (`src/components/general/bot-sessions-modal.tsx`) lists, searches, starts and reopens sessions, and the new-session icon beside it (`(partials)/new-session-button.tsx`) starts one (`api/admin/bots/list-sessions`, `new-session`, `reopen-session`). The rail lists the open bot's latest `AppData.RailRecentSessions` sessions under its row (`rail-bot-sessions.tsx`), and once there are more, "All sessions" opens the same modal. Unread, the rail's last message and activity are per thread, from its sessions. Job results go into the job's own session, even after it closes.
- **Coordinator:** the install has exactly one `kind: coordinator` bot, the super admin's alone, that coordinates the crew and delegates to every worker. It is created at signup (`bun run seed`) and, as a fallback, by the super admin's `/admin/bots`, all via `ensureCoordinatorBot` (`src/functions/backend/db/bots/ensure-coordinator-bot.ts`, idempotent). Name in `CoordinatorBotName` (`src/dict/bot-dict.ts`). Its default prompt is `CoordinatorSystemPrompt` (`src/data/coordinator-system-prompt.ts`): attached at creation, and backfilled onto a coordinator whose prompt is empty, never over an edited one.
- **Routes:** `/admin/bots` opens the bot this browser was last in (`readLastVisitedBot` from the single `cobble_last_visited_bot_id` cookie, which the conversation pages set through `withLastVisitedBotCookies` in their `res_transform`, and which `/auth/logout` clears; a conversation inside a workspace is never remembered, so the landing page always reopens the bot's own chat), but only while `grabReachableConversation` says the user still reaches it. Otherwise it ensures the coordinator and redirects the super admin to its chat (`/admin/bots/1` on a fresh install), anyone else to their latest bot, else their first workspace, else an empty page; `/admin/bots/[bot_id]` renders the conversation (header, thread and composer, the last two held to 760px; there is no right panel); `/admin/bots/[bot_id]/models` is a worker bot's own model settings, for super admins and admins (the coordinator's redirects to its chat); `/admin/workspaces/[workspace_id]` is a workspace's dashboard (`(sections)/workspace-dashboard.tsx`: read-only picture, name, description and context, latest activity, bots with their per-workspace instructions and memory, computers; its header links into `/admin/settings/workspaces/[workspace_id]`, whose `workspace-overview.tsx` has the editable groups) and `/admin/workspaces/[workspace_id]/bots/[bot_id]` is the same `Conversation` section scoped to the workspace (`pageProps.workspace`; only the coordinator and member bots). All render in the full-bleed `src/layouts/bots/` layout (rail of conversations + user popover), which `__root.tsx` uses for every `/admin/*` page.