Add workspaces, computers, bot memory and the bots queue
- Schema: workspaces (+ workspace_bots, workspace_computers), containers, computers, bot_computers, bots.memory, workspaces.system_prompt, and bots_queue as a bun-sqlite isQueue table - Main computer (and its container record) ensured with the coordinator on signup, seed and /admin/bots; the coordinator's access to it is locked - Effective computer access: direct grants plus workspace computers - Full bot prompt: core instructions, workspace prompts, then memory; bots can rewrite their memory and system prompt - Queue helpers on the native BunSQLite.queue API, plus a handler that heartbeats, serializes per computer, recovers after restarts, reports lost jobs, supports cancelling and pushes outcomes over the WebSocket - Enable SQLite foreign keys at startup so schema cascades fire - Prune finished jobs daily Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
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 type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* Opens the coordinator's conversation, creating the coordinator first if
|
||||
* this workspace doesn't have one yet.
|
||||
* Opens the coordinator's conversation, creating the coordinator and the
|
||||
* main computer first if this account doesn't have them yet.
|
||||
*/
|
||||
export const server: BunextPageServerFn<PagePropsType> = async ({ req }) => {
|
||||
const { user } = await userAuth({ req });
|
||||
@@ -19,6 +20,7 @@ export const server: BunextPageServerFn<PagePropsType> = async ({ req }) => {
|
||||
}
|
||||
|
||||
const coordinator_id = await ensureCoordinatorBot({ user_id: user.id });
|
||||
await ensureMainComputer({ user_id: user.id, coordinator_id });
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOT_COMPUTERS } from "@/db/types/db";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import isLockedComputerAccess from "@/src/utils/is-locked-computer-access";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
type AccessRow = BUN_SQLITE_COBBLE_BOT_COMPUTERS & {
|
||||
bot_kind?: string;
|
||||
computer_kind?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the matching access rows, skipping the coordinator's access to
|
||||
* the main computer.
|
||||
*/
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, id, query } = params;
|
||||
|
||||
const records_to_delete =
|
||||
await BunSQLite.select<BUN_SQLITE_COBBLE_BOT_COMPUTERS>({
|
||||
table,
|
||||
query: _.merge(query?.sql_query, body?.sql_query) || {},
|
||||
targetId: id,
|
||||
});
|
||||
|
||||
if (!records_to_delete.payload?.[0]) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No Records to Delete`,
|
||||
};
|
||||
}
|
||||
|
||||
let skipped = 0;
|
||||
|
||||
for (let i = 0; i < records_to_delete.payload.length; i++) {
|
||||
const record = records_to_delete.payload[i];
|
||||
|
||||
if (!record?.id) continue;
|
||||
|
||||
const access = (
|
||||
await BunSQLite.sql<AccessRow>({
|
||||
sql: `SELECT bc.id, b.kind AS bot_kind, c.kind AS computer_kind FROM bot_computers bc JOIN bots b ON b.id = bc.bot_id JOIN computers c ON c.id = bc.computer_id WHERE bc.id = ? LIMIT 1`,
|
||||
values: [record.id],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
if (
|
||||
isLockedComputerAccess({
|
||||
bot_kind: access?.bot_kind,
|
||||
computer_kind: access?.computer_kind,
|
||||
})
|
||||
) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
await BunSQLite.delete({
|
||||
table,
|
||||
targetId: record.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
msg: skipped
|
||||
? `The coordinator always has access to the main computer.`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import get from "../../(functions)/get";
|
||||
import post from "../../(functions)/post";
|
||||
import put from "./put";
|
||||
import del from "./del";
|
||||
|
||||
/**
|
||||
* Generic CRUD (super admin only), except that the coordinator's access to
|
||||
* the main computer can't be changed or removed.
|
||||
*/
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { req, user_types } = params;
|
||||
|
||||
const is_super_admin = checkUserAccess({ user_types });
|
||||
|
||||
if (!is_super_admin.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
};
|
||||
}
|
||||
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
return await get(params);
|
||||
case "POST":
|
||||
return await post(params);
|
||||
case "PUT":
|
||||
return await put(params);
|
||||
case "DELETE":
|
||||
return await del(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Can't update computer access. Can only delete and insert.`,
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import _ from "lodash";
|
||||
import users from "./(tables)/users";
|
||||
import userTypes from "./(tables)/user_types";
|
||||
import media from "./(tables)/media";
|
||||
import botComputers from "./(tables)/bot_computers";
|
||||
import get from "./(functions)/get";
|
||||
import put from "./(functions)/put";
|
||||
import del from "./(functions)/delete";
|
||||
@@ -83,6 +84,8 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
return await userTypes(crud_params);
|
||||
case "media":
|
||||
return await media(crud_params);
|
||||
case "bot_computers":
|
||||
return await botComputers(crud_params);
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -3,6 +3,7 @@ import setUserPassword from "@/src/functions/backend/auth/set-user-password";
|
||||
import grabHasUsers from "@/src/functions/backend/auth/grab-has-users";
|
||||
import loginUser from "@/src/functions/backend/auth/login-user";
|
||||
import ensureCoordinatorBot from "@/src/functions/backend/db/bots/ensure-coordinator-bot";
|
||||
import ensureMainComputer from "@/src/functions/backend/db/computers/ensure-main-computer";
|
||||
import validatePassword from "@/src/utils/validate-password";
|
||||
import logEvent, { grabRequestIP, logError } from "@/src/functions/backend/logger";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
@@ -74,16 +75,20 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
|
||||
/**
|
||||
* The account exists now, so a failure here mustn't fail signup
|
||||
* (which is one-shot). `/admin/bots` creates it on the next visit.
|
||||
* (which is one-shot). `/admin/bots` creates both on the next visit.
|
||||
*/
|
||||
await ensureCoordinatorBot({ user_id: new_user_id }).catch((error) =>
|
||||
logError({
|
||||
title: "Coordinator bot not created at signup",
|
||||
error,
|
||||
user_id: new_user_id,
|
||||
ip_address: grabRequestIP(req),
|
||||
}).catch(() => {}),
|
||||
);
|
||||
await ensureCoordinatorBot({ user_id: new_user_id })
|
||||
.then((coordinator_id) =>
|
||||
ensureMainComputer({ user_id: new_user_id, coordinator_id }),
|
||||
)
|
||||
.catch((error) =>
|
||||
logError({
|
||||
title: "Coordinator or main computer not created at signup",
|
||||
error,
|
||||
user_id: new_user_id,
|
||||
ip_address: grabRequestIP(req),
|
||||
}).catch(() => {}),
|
||||
);
|
||||
|
||||
await logEvent({
|
||||
title: "Super admin created",
|
||||
|
||||
Reference in New Issue
Block a user