Share one crew across the install, keep user memory per user
Bots, workspaces and computers now belong to the install. Users reach workers only through bot_users (assigned by a super admin or admin) and workspace_users; the coordinator and main computer are one per install, and only the super admin sees the coordinator. Conversations are per user (bot_sessions.user_id), jobs run for the session's user, and live updates go only to who should see them. A bot's core memory is now general knowledge shared by every user it works for; what it learns about a user lives in bot_user_memories. One-off scripts move existing installs over: move-bot-memory-to-owners (run first), then share-crew. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -39,6 +39,7 @@ export const handler: BunextAPIRouteHandler<
|
||||
return {
|
||||
success: true,
|
||||
payload: await grabBotSessions({
|
||||
user_id: user.id,
|
||||
bot_id: conversation.bot.id,
|
||||
workspace_id: conversation.workspace_id,
|
||||
query,
|
||||
|
||||
@@ -44,6 +44,7 @@ export const handler: BunextAPIRouteHandler<
|
||||
return {
|
||||
success: true,
|
||||
singleRes: await startBotSession({
|
||||
user_id: user.id,
|
||||
bot_id: conversation.bot.id,
|
||||
workspace_id: conversation.workspace_id,
|
||||
}),
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { BotHarnessOverviewPayload } from "@/src/types";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
||||
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
@@ -37,6 +38,10 @@ export const handler: BunextAPIRouteHandler<
|
||||
throw new AppError(`This user can only read records.`);
|
||||
}
|
||||
|
||||
if (!canManageCrew({ user_types })) {
|
||||
throw new AppError(`Only an admin can do that.`);
|
||||
}
|
||||
|
||||
const { bot_id, settings } = parseWithSchema({
|
||||
schema: SaveBotHarnessSettingsRequestSchema,
|
||||
data: body,
|
||||
|
||||
@@ -17,8 +17,8 @@ import type {
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* Posts a message from the session user to one of their own bots, in its
|
||||
* own chat or in a workspace it's in, and queues the bot's reply, which
|
||||
* Posts a message from the session user to a bot they chat with, in its
|
||||
* own chat or in a workspace, and queues the bot's reply, which
|
||||
* arrives over the WebSocket. The message goes in the conversation's
|
||||
* active session (the next one when it's full) and also to the user's
|
||||
* other open tabs. Files are uploaded first (`upload-attachment`) and
|
||||
@@ -57,6 +57,7 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
|
||||
const media = await grabAttachableMedia({ user_id: Number(user.id), media_ids });
|
||||
const session = await ensureBotSession({
|
||||
user_id: user.id,
|
||||
bot_id: bot.id,
|
||||
workspace_id: conversation.workspace_id,
|
||||
rollover: true,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import ensureCoordinatorBot from "@/src/functions/backend/db/bots/ensure-coordinator-bot";
|
||||
import ensureMainComputer from "@/src/functions/backend/db/computers/ensure-main-computer";
|
||||
import setUserAccess from "@/src/functions/backend/db/access/set-user-access";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import saveUser from "@/src/functions/backend/db/users/save-user";
|
||||
import setUserProfileImage from "@/src/functions/backend/db/users/set-user-profile-image";
|
||||
import logEvent, { grabRequestIP, logError } from "@/src/functions/backend/logger";
|
||||
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
||||
import { SaveUserRequestSchema } from "@/src/schemas/user-schema";
|
||||
import type { BUN_SQLITE_COBBLE_USERS_JOIN } from "@/src/types/sql-joins";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
@@ -22,8 +21,10 @@ import type {
|
||||
/**
|
||||
* Creates a user (`canCreateUser`) or updates one (`canUpdateUser`):
|
||||
* name, email, username, password, user types (each checked against the
|
||||
* assignment matrix) and, when one is sent, their profile photo. A new user gets their coordinator and main computer
|
||||
* straight away, as at signup. Returns the user.
|
||||
* assignment matrix), when one is sent their profile photo, and when sent
|
||||
* the workers assigned to them and the workspaces they're in
|
||||
* (`setUserAccess`). Whoever may create or update users manages the crew
|
||||
* too (super admins and admins). Returns the user.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject<BUN_SQLITE_COBBLE_USERS_JOIN>> = async ({ req, body }) => {
|
||||
try {
|
||||
@@ -64,21 +65,8 @@ export const handler: BunextAPIRouteHandler<APIResponseObject<BUN_SQLITE_COBBLE_
|
||||
await setUserProfileImage({ user_id, data_url: data.image });
|
||||
}
|
||||
|
||||
if (created) {
|
||||
/**
|
||||
* The account exists now; `/admin/bots` creates both on their
|
||||
* first visit if this fails.
|
||||
*/
|
||||
await ensureCoordinatorBot({ user_id })
|
||||
.then((coordinator_id) => ensureMainComputer({ user_id, coordinator_id }))
|
||||
.catch((error) =>
|
||||
logError({
|
||||
title: "Coordinator or main computer not created with the user",
|
||||
error,
|
||||
user_id,
|
||||
ip_address: grabRequestIP(req),
|
||||
}).catch(() => {}),
|
||||
);
|
||||
if (data.bot_ids || data.workspace_ids) {
|
||||
await setUserAccess({ user_id, bot_ids: data.bot_ids, workspace_ids: data.workspace_ids });
|
||||
}
|
||||
|
||||
await logEvent({
|
||||
@@ -86,7 +74,13 @@ export const handler: BunextAPIRouteHandler<APIResponseObject<BUN_SQLITE_COBBLE_
|
||||
category: "auth",
|
||||
user_id: user.id,
|
||||
plain_text: `User #${user.id} ${created ? "created" : "updated"} user #${user_id}.`,
|
||||
metadata: { user_id, user_types: data.user_types, password_changed: Boolean(data.password) },
|
||||
metadata: {
|
||||
user_id,
|
||||
user_types: data.user_types,
|
||||
password_changed: Boolean(data.password),
|
||||
bot_ids: data.bot_ids,
|
||||
workspace_ids: data.workspace_ids,
|
||||
},
|
||||
ip_address: grabRequestIP(req),
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { BotHarnessOverviewPayload } from "@/src/types";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
||||
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
@@ -38,6 +39,10 @@ export const handler: BunextAPIRouteHandler<
|
||||
throw new AppError(`This user can only read records.`);
|
||||
}
|
||||
|
||||
if (!canManageCrew({ user_types })) {
|
||||
throw new AppError(`Only an admin can do that.`);
|
||||
}
|
||||
|
||||
const { workspace_id, settings } = parseWithSchema({
|
||||
schema: SaveWorkspaceHarnessSettingsRequestSchema,
|
||||
data: body,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { UpdateWorkspaceBotRequestSchema } from "@/src/schemas/workspace-schema"
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
||||
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
@@ -32,6 +33,10 @@ export const handler: BunextAPIRouteHandler<APIResponseObject<WorkspaceBot>> = a
|
||||
throw new AppError(`This user can only read records.`);
|
||||
}
|
||||
|
||||
if (!canManageCrew({ user_types })) {
|
||||
throw new AppError(`Only an admin can do that.`);
|
||||
}
|
||||
|
||||
const { workspace_id, bot_id, system_prompt, memory } = parseWithSchema({ schema: UpdateWorkspaceBotRequestSchema, data: body });
|
||||
|
||||
await updateWorkspaceBot({ user_id: user.id, workspace_id, bot_id, system_prompt, memory });
|
||||
|
||||
Reference in New Issue
Block a user