Add the harness, envs, per-bot models and chat attachments

- Harness: providers, model catalog synced hourly, automatic free picks,
  cooldowns, tool loop with context trimming, MCP computer tools
- Bots queue runs replies and tasks through the harness; jobs wait
  (attempt refunded) while every model is resting
- Envs: Settings > Environment, scoped general/workspace/computer,
  written to computers and redacted from tool output
- Models: Settings > Models, per-harness provider select defaulting to
  OpenRouter, and a models modal on each bot's chat (the coordinator's
  edits the defaults)
- Chat: thinking row while a reply runs, file attachments (upload,
  paste, drop) that replies read as images, audio and document text
- Fix media uploads failing after a user's first file, and private
  files being served from the public media path

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 03:38:38 +01:00
co-authored by Claude Opus 5.5
parent d49f524fe8
commit 6a2665dae9
223 changed files with 12342 additions and 162 deletions
@@ -1,4 +1,5 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BotJobKind } from "@/src/dict/bot-queue-dict";
import type {
BUN_SQLITE_COBBLE_BOTS_QUEUE,
BunSQLiteTables,
@@ -11,21 +12,43 @@ type Params = {
*/
worker: string;
limit?: number;
/**
* Only jobs of this kind, so replies and tasks get their own slots.
*/
kind: BotJobKind;
};
/**
* Claims the next pending jobs for a queue handler. Jobs whose handler
* stopped sending updates are released back to the queue first, so a lost
* job is picked up again (resume from its `checkpoint`).
* Claims the next pending jobs of one kind for a queue handler. Jobs
* whose handler stopped sending updates are released back to the queue
* first, so a lost job is picked up again (resume from its `checkpoint`).
*
* bun-sqlite's `queue.claim` takes no filter, so the claim is its same
* single statement (atomic: two workers never claim one job) plus `kind`.
*/
export default async function claimBotJobs({ worker, limit = 1 }: Params) {
const res = await BunSQLite.queue.claim<
export default async function claimBotJobs({ worker, limit = 1, kind }: Params) {
const released = await BunSQLite.queue.release_stale<
BUN_SQLITE_COBBLE_BOTS_QUEUE,
(typeof BunSQLiteTables)[number]
>({
table: "bots_queue",
worker,
limit,
>({ table: "bots_queue" });
if (!released.success) {
throw new Error(`Couldn't release stale jobs: ${released.msg || released.error}`);
}
if (limit < 1) return [];
const now = Date.now();
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS_QUEUE>({
sql: `UPDATE bots_queue SET status = 'processing', attempts = attempts + 1,
locked_at = ?, locked_by = ?, updated_at = ?
WHERE id IN (
SELECT id FROM bots_queue
WHERE status = 'pending' AND available_at <= ? AND kind = ?
ORDER BY id LIMIT ?
) RETURNING *`,
values: [now, worker, now, now, kind, Math.floor(limit)],
});
if (!res.success) {
@@ -17,12 +17,17 @@ type Params = {
* Flag the message as needing the user.
*/
needs_you?: boolean;
/**
* Finish without posting a message.
*/
silent?: boolean;
};
/**
* Marks a job done, closes its task and sends the completion message back
* to the user in the bot's conversation. The bot shows done (or needs-you)
* unless it has other jobs open. Throws if this handler no longer holds
* to the user in the bot's conversation. Unless it has other jobs open,
* the bot shows needs-you if asked, else done after a task and idle after
* a reply. Throws if this handler no longer holds
* the job.
*/
export default async function completeBotJob({
@@ -30,6 +35,7 @@ export default async function completeBotJob({
worker,
result,
needs_you,
silent,
}: Params) {
const job = await grabLockedBotJob({ job_id, worker });
@@ -60,11 +66,13 @@ export default async function completeBotJob({
});
}
const message = await insertBotMessage({
bot_id: job.bot_id,
text: result,
needs_you,
});
const message = silent
? null
: await insertBotMessage({
bot_id: job.bot_id,
text: result,
needs_you,
});
const bot = needs_you
? await setBotFace({
@@ -74,7 +82,10 @@ export default async function completeBotJob({
})
: (await countOpenBotJobs({ bot_id: job.bot_id }))
? null
: await setBotFace({ bot_id: job.bot_id, face: "done" });
: await setBotFace({
bot_id: job.bot_id,
face: job.kind == "reply" ? "idle" : "done",
});
return { message, bot };
}
@@ -0,0 +1,37 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import { AppData } from "@/src/data/app-data";
type Params = {
job_id: number | string;
worker: string;
/**
* Unix ms before which the job can't be claimed again.
*/
until: number;
/**
* Give back the attempt this claim used: nothing was tried.
*/
refund?: boolean;
};
/**
* Holds a job back without failing it: a pending job whose `available_at`
* is pushed to `until` (capped at `AppData.HarnessMaxCooldownMs` from
* now), or, for a job still held by `worker`, released back to pending
* that way. False when the job is neither.
*/
export default async function deferBotJob({ job_id, worker, until, refund }: Params) {
const now = Date.now();
const at = Math.min(Math.max(until, now), now + AppData.HarnessMaxCooldownMs);
const res = await BunSQLite.sql({
sql: `UPDATE bots_queue SET status = 'pending', locked_at = NULL, locked_by = NULL,
attempts = CASE WHEN ? AND status = 'processing' THEN MAX(attempts - 1, 0) ELSE attempts END,
available_at = MAX(COALESCE(available_at, 0), ?), updated_at = ?
WHERE id = ? AND (status = 'pending' OR (status = 'processing' AND locked_by = ?))
RETURNING id`,
values: [refund ? 1 : 0, at, now, job_id, worker],
});
return Boolean(res.payload?.length);
}
@@ -4,11 +4,17 @@ import type {
BUN_SQLITE_COBBLE_BOTS_QUEUE,
BunSQLiteTables,
} from "@/db/types/db";
import type { BotJobKind } from "@/src/dict/bot-queue-dict";
import AppError from "@/src/utils/app-error";
import grabUserBot from "../bots/grab-user-bot";
import grabBotComputers from "../computers/grab-bot-computers";
type Params = {
/**
* `task` (the default) shows in the working-on panel; a `reply` answers
* the user in chat and has no task.
*/
kind?: BotJobKind;
user_id: number | string;
bot_id: number | string;
title: string;
@@ -24,7 +30,7 @@ type Params = {
message_id?: number | string;
/**
* The task for the working-on panel. One is created from `title` when
* not given.
* not given (replies have none).
*/
task_id?: number | string;
/**
@@ -44,6 +50,7 @@ type Params = {
* every message needs a job; quick answers are replied to directly.
*/
export default async function enqueueBotJob({
kind = "task",
user_id,
bot_id,
title,
@@ -76,7 +83,10 @@ export default async function enqueueBotJob({
}
}
const final_task_id = task_id || (await createJobTask({ bot_id: bot.id, title }));
const final_task_id =
kind == "reply"
? undefined
: task_id || (await createJobTask({ bot_id: bot.id, title }));
const res = await BunSQLite.queue.add<
BUN_SQLITE_COBBLE_BOTS_QUEUE,
@@ -85,12 +95,13 @@ export default async function enqueueBotJob({
table: "bots_queue",
data: [
{
kind,
bot_id: bot.id,
title,
instructions,
params: params ? JSON.stringify(params) : undefined,
message_id: message_id ? Number(message_id) : undefined,
task_id: Number(final_task_id),
task_id: final_task_id ? Number(final_task_id) : undefined,
computer_id: computer_id ? Number(computer_id) : undefined,
},
],
@@ -0,0 +1,51 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_BOTS_QUEUE } from "@/db/types/db";
import enqueueBotJob from "./enqueue-bot-job";
import { BotReplyJobTitle } from "@/src/dict/bot-queue-dict";
type Params = {
user_id: number | string;
bot_id: number | string;
/**
* The user message to answer.
*/
message_id: number | string;
};
/**
* Queues the bot's chat reply to the user's latest message(s). A reply
* reads the whole recent conversation when it runs, so a burst of
* messages shares one reply: while one is still pending, it is reused.
*/
export default async function enqueueBotReply({
user_id,
bot_id,
message_id,
}: Params) {
const pending = (
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS_QUEUE>({
sql: `SELECT id FROM bots_queue
WHERE bot_id = ? AND kind = 'reply' AND status = 'pending'
ORDER BY id DESC LIMIT 1`,
values: [bot_id],
})
).payload?.[0];
if (pending?.id) {
await BunSQLite.sql({
sql: `UPDATE bots_queue SET message_id = ? WHERE id = ?`,
values: [message_id, pending.id],
});
return pending.id;
}
return await enqueueBotJob({
kind: "reply",
user_id,
bot_id,
title: BotReplyJobTitle,
instructions: "Reply to the user's latest messages.",
message_id,
});
}
@@ -30,7 +30,12 @@ export default async function notifyBotJobFailed({
bot_id: job.bot_id,
text:
failure_message ||
[`I couldn't finish "${job.title || "the job"}".`, job.last_error]
[
job.kind == "reply"
? `I couldn't reply.`
: `I couldn't finish "${job.title || "the job"}".`,
job.last_error,
]
.filter(Boolean)
.join(" "),
needs_you: true,
@@ -0,0 +1,33 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
import AppError from "@/src/utils/app-error";
type Params = {
user_id: number;
media_ids: number[];
};
/**
* The session user's own uploaded files, in the given order. Throws when
* any isn't theirs (or no longer exists), so a message can't carry
* someone else's file.
*/
export default async function grabAttachableMedia({ user_id, media_ids }: Params) {
const ids = [...new Set(media_ids)];
if (!ids.length) return [];
const rows =
(
await BunSQLite.sql<BUN_SQLITE_COBBLE_MEDIA>({
sql: `SELECT * FROM media WHERE user_id = ? AND id IN (${ids.map(() => "?").join(", ")})`,
values: [user_id, ...ids],
})
).payload || [];
if (rows.length != ids.length) {
throw new AppError(`One of the files is gone. Attach it again.`);
}
return ids.map((id) => rows.find((row) => row.id == id)!);
}
@@ -0,0 +1,21 @@
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
import BunSQLite from "@moduletrace/bun-sqlite";
type Params = {
bot_id: number | string;
};
/**
* A bot by id, whoever owns it. Server-side work only (the queue); request
* handlers use `grabUserBot` to check ownership.
*/
export default async function grabBotById({
bot_id,
}: Params): Promise<BUN_SQLITE_COBBLE_BOTS | null> {
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
sql: `SELECT * FROM bots WHERE id = ? LIMIT 1`,
values: [bot_id],
});
return res.payload?.[0] || null;
}
@@ -1,5 +1,8 @@
import type { BUN_SQLITE_COBBLE_BOT_MESSAGES } from "@/db/types/db";
import type { BotMessageWithAttachments } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import grabMessagesMedia from "./grab-messages-media";
import mediaToAttachment from "./media-to-attachment";
type Params = {
bot_id: number | string;
@@ -7,12 +10,13 @@ type Params = {
};
/**
* The latest messages of a bot's conversation, oldest first.
* The latest messages of a bot's conversation, oldest first, with their
* attachments.
*/
export default async function grabBotMessages({
bot_id,
limit = 100,
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_MESSAGES[]> {
}: Params): Promise<BotMessageWithAttachments[]> {
let sql = ``;
sql += `SELECT * FROM (\n`;
@@ -27,5 +31,16 @@ export default async function grabBotMessages({
values: [bot_id, limit],
});
return res.payload || [];
const messages = res.payload || [];
const media = await grabMessagesMedia({
message_ids: messages.map((message) => Number(message.id)),
});
return messages.map((message) => {
const files = media.get(Number(message.id));
return files
? { ...message, attachments: files.map((file) => mediaToAttachment({ media: file })) }
: message;
});
}
@@ -0,0 +1,38 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
type Params = {
message_ids: number[];
};
export type MessageMediaRow = BUN_SQLITE_COBBLE_MEDIA & { message_id: number };
/**
* The files attached to these messages, by message id, in the order they
* were attached.
*/
export default async function grabMessagesMedia({ message_ids }: Params) {
const by_message = new Map<number, MessageMediaRow[]>();
const ids = [...new Set(message_ids.filter(Boolean))];
if (!ids.length) return by_message;
const rows =
(
await BunSQLite.sql<MessageMediaRow>({
sql: `SELECT m.*, l.message_id FROM bot_message_media l
JOIN media m ON m.id = l.media_id
WHERE l.message_id IN (${ids.map(() => "?").join(", ")})
ORDER BY l.id ASC`,
values: ids,
})
).payload || [];
for (const row of rows) {
const list = by_message.get(row.message_id) || [];
list.push(row);
by_message.set(row.message_id, list);
}
return by_message;
}
@@ -1,22 +1,32 @@
import type {
BUN_SQLITE_COBBLE_BOT_MESSAGES,
BUN_SQLITE_COBBLE_MEDIA,
BunSQLiteTables,
} from "@/db/types/db";
import type { BotMessageWithAttachments } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import mediaToAttachment from "./media-to-attachment";
type Params = {
bot_id: number;
text: string;
/**
* Files to attach, already checked to be the user's own
* (`grabAttachableMedia`).
*/
media?: BUN_SQLITE_COBBLE_MEDIA[];
};
/**
* Appends a user message to a bot's conversation and bumps its activity.
* Callers must check the bot belongs to the session user first.
* Appends a user message (and its files) to a bot's conversation and
* bumps its activity. Callers must check the bot belongs to the session
* user first.
*/
export default async function insertUserMessage({
bot_id,
text,
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_MESSAGES> {
media = [],
}: Params): Promise<BotMessageWithAttachments> {
const now = Date.now();
const res = await BunSQLite.insert<
@@ -33,6 +43,13 @@ export default async function insertUserMessage({
throw new Error(`Failed to save message: ${res.msg}`);
}
for (const file of media) {
await BunSQLite.sql({
sql: `INSERT INTO bot_message_media (created_at, updated_at, message_id, media_id) VALUES (?, ?, ?, ?)`,
values: [now, now, id, file.id!],
});
}
await BunSQLite.sql({
sql: `UPDATE bots SET last_activity_at = ? WHERE id = ?`,
values: [now, bot_id],
@@ -46,5 +63,6 @@ export default async function insertUserMessage({
needs_you: 0,
created_at: now,
updated_at: now,
attachments: media.map((file) => mediaToAttachment({ media: file })),
};
}
@@ -0,0 +1,23 @@
import type { BUN_SQLITE_COBBLE_MEDIA } from "@/db/types/db";
import grabMediaURL from "@/src/functions/backend/media/grab-media-url";
import type { BotMessageAttachment, MediaType } from "@/src/types";
type Params = {
media: BUN_SQLITE_COBBLE_MEDIA;
};
/**
* A `media` row as a chat attachment: its original name (`title`), type
* and the private URLs it's served at.
*/
export default function mediaToAttachment({ media }: Params): BotMessageAttachment {
const { media_url, media_thumbnail_url } = grabMediaURL({ media: { ...media } });
return {
id: Number(media.id),
name: media.title || `File ${media.id}`,
media_type: (media.media_type || "document") as MediaType,
url: media_url || "",
thumbnail_url: media_thumbnail_url,
};
}
@@ -0,0 +1,57 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type {
BUN_SQLITE_COBBLE_COMPUTERS,
BUN_SQLITE_COBBLE_CONTAINERS,
} from "@/db/types/db";
import parseJobJson from "@/src/bots-queue/parse-job-json";
import { AppData } from "@/src/data/app-data";
import { ComputerMcpPath } from "@/src/dict/computer-dict";
import grabEnv from "../envs/grab-env";
type Params = {
computer_id: number | string;
};
type ComputerRow = Pick<BUN_SQLITE_COBBLE_COMPUTERS, "id" | "name" | "kind"> &
Pick<BUN_SQLITE_COBBLE_CONTAINERS, "hostname" | "port" | "metadata">;
/**
* Where a computer's MCP server answers. In order: the container's
* `metadata.mcp_url`, `MAIN_COMPUTER_URL` for the main computer (an app
* setting, `grabEnv`: for a server outside docker, e.g.
* `http://localhost:8170`), else its docker hostname and port. `base_url`
* is the same address without `/mcp`, for the sandbox's REST API. Null
* when the computer has no container.
*/
export default async function grabComputerMcpUrl({ computer_id }: Params) {
const row = (
await BunSQLite.sql<ComputerRow>({
sql: `SELECT co.id, co.name, co.kind, c.hostname, c.port, c.metadata
FROM computers co JOIN containers c ON c.id = co.container_id
WHERE co.id = ? LIMIT 1`,
values: [computer_id],
})
).payload?.[0];
if (!row?.hostname) return null;
const metadata = parseJobJson({ text: row.metadata }) || {};
if (typeof metadata.mcp_url == "string" && metadata.mcp_url) {
return {
url: metadata.mcp_url,
base_url: metadata.mcp_url.replace(/\/mcp\/?$/, ""),
name: row.name || "",
};
}
const main_override = row.kind == "main"
? (await grabEnv({ key: "MAIN_COMPUTER_URL" }))?.trim()
: undefined;
const base = main_override
? main_override.replace(/\/+$/, "")
: `http://${row.hostname}:${row.port || AppData.ComputerPort}`;
return { url: `${base}${ComputerMcpPath}`, base_url: base, name: row.name || "" };
}
@@ -0,0 +1,21 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import { clearEnvCache } from "./grab-all-envs";
type Params = {
env_id: number;
};
/**
* Removes an env. Computers drop it on their next run, when their env file
* is rewritten.
*/
export default async function deleteEnv({ env_id }: Params) {
const res = await BunSQLite.sql({
sql: `DELETE FROM envs WHERE id = ?`,
values: [env_id],
});
if (!res.success) throw new Error(`Couldn't remove the env: ${res.error}`);
clearEnvCache();
}
@@ -0,0 +1,59 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_ENVS } from "@/db/types/db";
import type { EnvParadigm } from "@/src/dict/env-dict";
import { decrypt } from "@/src/utils/crypt";
export type DecryptedEnv = {
id: number;
key: string;
value: string;
description: string;
paradigm: EnvParadigm;
workspace_id: number | null;
computer_id: number | null;
secret: boolean;
expose_to_computers: boolean;
};
let cache: Promise<DecryptedEnv[]> | undefined;
/**
* Every env, decrypted, cached in this process until `clearEnvCache` (every
* save and delete calls it). A value that can't be decrypted (the
* encryption key changed) reads as empty.
*/
export default function grabAllEnvs(): Promise<DecryptedEnv[]> {
cache ||= load().catch((error) => {
cache = undefined;
throw error;
});
return cache;
}
export function clearEnvCache() {
cache = undefined;
}
async function load() {
const rows =
(
await BunSQLite.sql<BUN_SQLITE_COBBLE_ENVS>({
sql: `SELECT * FROM envs ORDER BY id ASC`,
})
).payload || [];
return await Promise.all(
rows.map(async (row): Promise<DecryptedEnv> => ({
id: Number(row.id),
key: row.key || "",
value: row.value ? await decrypt(row.value).catch(() => "") : "",
description: row.description || "",
paradigm: (row.paradigm || "general") as EnvParadigm,
workspace_id: typeof row.workspace_id == "number" ? row.workspace_id : null,
computer_id: typeof row.computer_id == "number" ? row.computer_id : null,
secret: row.secret != 0,
expose_to_computers: row.expose_to_computers != 0,
})),
);
}
@@ -0,0 +1,36 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import grabAllEnvs from "./grab-all-envs";
import resolveScopedEnvs from "./resolve-scoped-envs";
type Params = {
computer_id: number | string;
/**
* The bot using the computer. Only workspaces it shares with the
* computer apply; without a bot, every workspace the computer is in.
*/
bot_id?: number | string;
};
/**
* The envs a run on this computer gets (`resolveScopedEnvs`). Workspaces
* are non-archived ones, oldest first, so the oldest wins a tie.
*/
export default async function grabComputerEnvs({ computer_id, bot_id }: Params) {
const workspaces =
(
await BunSQLite.sql<{ id: number }>({
sql: `SELECT w.id FROM workspaces w
JOIN workspace_computers wc ON wc.workspace_id = w.id AND wc.computer_id = ?
WHERE w.archived = 0
${bot_id ? `AND w.id IN (SELECT workspace_id FROM workspace_bots WHERE bot_id = ?)` : ""}
ORDER BY w.id ASC`,
values: bot_id ? [computer_id, bot_id] : [computer_id],
})
).payload || [];
return resolveScopedEnvs({
envs: await grabAllEnvs(),
computer_id: Number(computer_id),
workspace_ids: workspaces.map((workspace) => Number(workspace.id)),
});
}
+17
View File
@@ -0,0 +1,17 @@
import grabAllEnvs from "./grab-all-envs";
type Params = {
key: string;
};
/**
* An app setting (an `EnvDict` key): the general value set in Settings,
* else the process environment, else undefined.
*/
export default async function grabEnv({ key }: Params) {
const saved = (await grabAllEnvs()).find(
(env) => env.paradigm == "general" && env.key == key,
);
return saved?.value || process.env[key] || undefined;
}
@@ -0,0 +1,77 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type {
BUN_SQLITE_COBBLE_COMPUTERS,
BUN_SQLITE_COBBLE_WORKSPACES,
} from "@/db/types/db";
import { EnvDict } from "@/src/dict/env-dict";
import type { EnvsOverviewPayload } from "@/src/types";
import grabAllEnvs from "./grab-all-envs";
type Params = {
user_id: number;
};
/**
* The Environment settings page: where envs can be set (general, the
* user's workspaces and computers), every env (secret values left out),
* and the app settings with where each comes from.
*/
export default async function grabEnvsOverview({
user_id,
}: Params): Promise<EnvsOverviewPayload> {
const [workspaces, computers, envs] = await Promise.all([
BunSQLite.sql<BUN_SQLITE_COBBLE_WORKSPACES>({
sql: `SELECT id, name, archived FROM workspaces WHERE user_id = ? ORDER BY archived ASC, id ASC`,
values: [user_id],
}),
BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
sql: `SELECT id, name, kind FROM computers WHERE user_id = ? ORDER BY kind = 'main' DESC, id ASC`,
values: [user_id],
}),
grabAllEnvs(),
]);
const workspace_ids = new Set((workspaces.payload || []).map((w) => Number(w.id)));
const computer_ids = new Set((computers.payload || []).map((c) => Number(c.id)));
const known_keys = new Set(EnvDict.map((env) => env.key));
const visible = envs.filter(
(env) =>
env.paradigm == "general" ||
(env.workspace_id !== null && workspace_ids.has(env.workspace_id)) ||
(env.computer_id !== null && computer_ids.has(env.computer_id)),
);
return {
scopes: [
{ paradigm: "general", id: null, title: "General" },
...(workspaces.payload || []).map((workspace) => ({
paradigm: "workspace" as const,
id: Number(workspace.id),
title: `Workspace: ${workspace.name}${workspace.archived ? " (archived)" : ""}`,
})),
...(computers.payload || []).map((computer) => ({
paradigm: "computer" as const,
id: Number(computer.id),
title: `Computer: ${computer.name || computer.kind}`,
})),
],
envs: visible.map((env) => ({
id: env.id,
key: env.key,
description: env.description,
paradigm: env.paradigm,
workspace_id: env.workspace_id,
computer_id: env.computer_id,
secret: env.secret,
expose_to_computers: env.expose_to_computers,
value: env.secret ? undefined : env.value,
known: known_keys.has(env.key),
})),
known: EnvDict.map((env) => ({
...env,
saved: envs.some((saved) => saved.paradigm == "general" && saved.key == env.key),
from_process: Boolean(process.env[env.key]),
})),
};
}
@@ -0,0 +1,69 @@
import { describe, expect, test } from "bun:test";
import type { DecryptedEnv } from "./grab-all-envs";
import resolveScopedEnvs from "./resolve-scoped-envs";
let next_id = 1;
const env = (fields: Partial<DecryptedEnv> & { key: string; value: string }): DecryptedEnv => ({
id: next_id++,
description: "",
paradigm: "general",
workspace_id: null,
computer_id: null,
secret: true,
expose_to_computers: true,
...fields,
});
const values = (envs: DecryptedEnv[]) =>
Object.fromEntries(envs.map((e) => [e.key, e.value]));
describe("resolveScopedEnvs", () => {
test("computer beats workspace beats general", () => {
const envs = [
env({ key: "TOKEN", value: "general" }),
env({ key: "TOKEN", value: "workspace", paradigm: "workspace", workspace_id: 2 }),
env({ key: "TOKEN", value: "computer", paradigm: "computer", computer_id: 7 }),
env({ key: "REGION", value: "eu", paradigm: "workspace", workspace_id: 2 }),
env({ key: "ONLY_GENERAL", value: "yes" }),
];
expect(values(resolveScopedEnvs({ envs, computer_id: 7, workspace_ids: [2] }))).toEqual({
TOKEN: "computer",
REGION: "eu",
ONLY_GENERAL: "yes",
});
});
test("ignores other computers and workspaces", () => {
const envs = [
env({ key: "TOKEN", value: "general" }),
env({ key: "TOKEN", value: "other", paradigm: "computer", computer_id: 8 }),
env({ key: "TOKEN", value: "elsewhere", paradigm: "workspace", workspace_id: 3 }),
];
expect(values(resolveScopedEnvs({ envs, computer_id: 7, workspace_ids: [2] }))).toEqual({
TOKEN: "general",
});
});
test("the first workspace listed wins a tie", () => {
const envs = [
env({ key: "TOKEN", value: "first", paradigm: "workspace", workspace_id: 2 }),
env({ key: "TOKEN", value: "second", paradigm: "workspace", workspace_id: 5 }),
];
expect(values(resolveScopedEnvs({ envs, computer_id: 7, workspace_ids: [2, 5] }))).toEqual({
TOKEN: "first",
});
});
test("leaves out unexposed envs and app settings", () => {
const envs = [
env({ key: "HIDDEN", value: "x", expose_to_computers: false }),
env({ key: "OPENROUTER_API_KEY", value: "sk-or" }),
];
expect(resolveScopedEnvs({ envs, computer_id: 7, workspace_ids: [] })).toEqual([]);
});
});
@@ -0,0 +1,42 @@
import { EnvDict } from "@/src/dict/env-dict";
import type { DecryptedEnv } from "./grab-all-envs";
type Params = {
envs: DecryptedEnv[];
computer_id: number;
/**
* The workspaces whose envs apply, in order of precedence (first wins).
*/
workspace_ids: number[];
};
/**
* The envs that reach one computer, one per key: its own beat its
* workspaces' (the first listed workspace wins a tie), which beat general
* ones. Only envs exposed to computers; app settings never are.
*/
export default function resolveScopedEnvs({
envs,
computer_id,
workspace_ids,
}: Params): DecryptedEnv[] {
const app_keys = new Set(EnvDict.map((env) => env.key));
const exposed = envs.filter(
(env) => env.expose_to_computers && !app_keys.has(env.key),
);
const resolved = new Map<string, DecryptedEnv>();
const layers = [
exposed.filter((env) => env.paradigm == "general"),
...[...workspace_ids].reverse().map((id) =>
exposed.filter((env) => env.paradigm == "workspace" && env.workspace_id == id),
),
exposed.filter((env) => env.paradigm == "computer" && env.computer_id == computer_id),
];
for (const layer of layers) {
for (const env of layer) resolved.set(env.key, env);
}
return [...resolved.values()].sort((a, b) => a.key.localeCompare(b.key));
}
+172
View File
@@ -0,0 +1,172 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_ENVS } from "@/db/types/db";
import {
EnvDict,
EnvKeyRegex,
ProcessOnlyEnvKeys,
type EnvParadigm,
} from "@/src/dict/env-dict";
import AppError from "@/src/utils/app-error";
import { encrypt } from "@/src/utils/crypt";
import { clearEnvCache } from "./grab-all-envs";
type Params = {
/**
* Owner of the workspace or computer the env is scoped to.
*/
user_id: number;
/**
* Edits this env; adds one when not given.
*/
env_id?: number;
key: string;
/**
* Required when adding. Left out when editing, the saved value stays.
*/
value?: string;
description?: string;
paradigm: EnvParadigm;
workspace_id?: number | null;
computer_id?: number | null;
secret: boolean;
expose_to_computers: boolean;
};
/**
* Adds or edits an env. One per key and scope: SQLite unique constraints
* treat NULL ids as distinct, so the check is here. App settings
* (`EnvDict`) are general only, never go to computers, and keep their
* dict's secret flag.
*/
export default async function saveEnv({
user_id,
env_id,
key,
value,
description,
paradigm,
workspace_id,
computer_id,
secret,
expose_to_computers,
}: Params) {
const final_key = key.trim();
if (!EnvKeyRegex.test(final_key)) {
throw new AppError(`Use letters, digits and underscores for the key, starting with a letter or underscore.`);
}
if ((ProcessOnlyEnvKeys as readonly string[]).includes(final_key)) {
throw new AppError(`${final_key} must stay in the server's environment.`);
}
const known = EnvDict.find((env) => env.key == final_key);
if (known && paradigm != "general") {
throw new AppError(`${final_key} is an app setting, so it can only be set in General.`);
}
const scope = await resolveScope({ user_id, paradigm, workspace_id, computer_id });
const duplicate = (
await BunSQLite.sql<BUN_SQLITE_COBBLE_ENVS>({
sql: `SELECT id FROM envs WHERE key = ? AND paradigm = ?
AND COALESCE(workspace_id, 0) = ? AND COALESCE(computer_id, 0) = ?
AND id != ? LIMIT 1`,
values: [final_key, paradigm, scope.workspace_id ?? 0, scope.computer_id ?? 0, env_id ?? 0],
})
).payload?.[0];
if (duplicate) {
throw new AppError(`${final_key} is already set here. Edit that one instead.`);
}
const fields = {
key: final_key,
description: description?.trim() || known?.description || "",
paradigm,
workspace_id: scope.workspace_id,
computer_id: scope.computer_id,
secret: (known ? known.secret : secret) ? 1 : 0,
expose_to_computers: !known && expose_to_computers ? 1 : 0,
};
if (env_id) {
const existing = (
await BunSQLite.sql<BUN_SQLITE_COBBLE_ENVS>({
sql: `SELECT id FROM envs WHERE id = ? LIMIT 1`,
values: [env_id],
})
).payload?.[0];
if (!existing) throw new AppError(`That env no longer exists.`);
const sets = Object.keys(fields).map((field) => `${field} = ?`);
const values: unknown[] = Object.values(fields);
if (value !== undefined) {
sets.push(`value = ?`);
values.push(await encrypt(value));
}
const res = await BunSQLite.sql({
sql: `UPDATE envs SET ${sets.join(", ")}, updated_at = ? WHERE id = ?`,
values: [...values, Date.now(), env_id] as any[],
});
if (!res.success) throw new Error(`Couldn't save the env: ${res.error}`);
} else {
if (value === undefined) {
throw new AppError(`Enter a value.`);
}
const row = { ...fields, value: await encrypt(value) };
const now = Date.now();
const res = await BunSQLite.sql({
sql: `INSERT INTO envs (created_at, updated_at, ${Object.keys(row).join(", ")})
VALUES (?, ?, ${Object.keys(row).map(() => "?").join(", ")})`,
values: [now, now, ...Object.values(row)] as any[],
});
if (!res.success) throw new Error(`Couldn't save the env: ${res.error}`);
}
clearEnvCache();
}
async function resolveScope({
user_id,
paradigm,
workspace_id,
computer_id,
}: {
user_id: number;
paradigm: EnvParadigm;
workspace_id?: number | null;
computer_id?: number | null;
}) {
if (paradigm == "general") {
return { workspace_id: null, computer_id: null };
}
const table = paradigm == "workspace" ? "workspaces" : "computers";
const id = paradigm == "workspace" ? workspace_id : computer_id;
const owned = id
? (
await BunSQLite.sql({
sql: `SELECT id FROM ${table} WHERE id = ? AND user_id = ? LIMIT 1`,
values: [id, user_id],
})
).payload?.[0]
: undefined;
if (!owned) {
throw new AppError(`Pick one of your ${table}.`);
}
return paradigm == "workspace"
? { workspace_id: Number(id), computer_id: null }
: { workspace_id: null, computer_id: Number(id) };
}
@@ -0,0 +1,40 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_HARNESS_MODELS } from "@/db/types/db";
import type { HarnessKind } from "@/src/dict/harness-dict";
import modelServesHarness from "@/src/harness/catalog/model-serves-harness";
import AppError from "@/src/utils/app-error";
import harnessModelRowToCatalog from "./harness-model-row-to-catalog";
type Params = {
harness: HarnessKind;
model_id: number;
/**
* The provider the setting is limited to, if any.
*/
provider_id?: number | null;
};
/**
* Throws unless the model is available, can do the harness's job and,
* when the setting is limited to a provider, comes from it.
*/
export default async function assertHarnessModel({ harness, model_id, provider_id }: Params) {
const row = (
await BunSQLite.sql<BUN_SQLITE_COBBLE_HARNESS_MODELS>({
sql: `SELECT * FROM harness_models WHERE id = ? AND available = 1 LIMIT 1`,
values: [model_id],
})
).payload?.[0];
if (!row) {
throw new AppError(`That model isn't available anymore.`);
}
if (provider_id && row.provider_id != provider_id) {
throw new AppError(`That model isn't from the picked provider.`);
}
if (!modelServesHarness({ model: harnessModelRowToCatalog({ row }), harness })) {
throw new AppError(`That model can't do this job.`);
}
}
@@ -0,0 +1,17 @@
import AppError from "@/src/utils/app-error";
import grabHarnessProviders from "./grab-harness-providers";
type Params = {
provider_id: number;
};
/**
* Throws unless the provider exists and is turned on.
*/
export default async function assertHarnessProvider({ provider_id }: Params) {
const provider = (await grabHarnessProviders({ provider_id }))[0];
if (!provider?.id || !provider.enabled) {
throw new AppError(`That provider isn't available. Turn it on in Settings, under Models.`);
}
}
@@ -0,0 +1,37 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import AppError from "@/src/utils/app-error";
import grabHarnessProviders from "./grab-harness-providers";
type Params = {
provider_id: number;
};
/**
* Removes a provider and its models. Harnesses (default and per bot) that
* picked one of its models, or were limited to it, go back to automatic
* on any provider.
*/
export default async function deleteHarnessProvider({ provider_id }: Params) {
const provider = (await grabHarnessProviders({ provider_id }))[0];
if (!provider?.id) {
throw new AppError(`Provider not found.`);
}
for (const table of ["harness_settings", "bot_harness_settings"]) {
await BunSQLite.sql({
sql: `UPDATE ${table} SET model_id = NULL, provider_id = NULL, updated_at = ?
WHERE provider_id = ? OR model_id IN (SELECT id FROM harness_models WHERE provider_id = ?)`,
values: [Date.now(), provider.id, provider.id],
});
}
const res = await BunSQLite.sql({
sql: `DELETE FROM harness_providers WHERE id = ?`,
values: [provider.id],
});
if (!res.success) {
throw new Error(`Couldn't remove the provider: ${res.error}`);
}
}
@@ -0,0 +1,41 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type {
BUN_SQLITE_COBBLE_HARNESS_PROVIDERS,
BunSQLiteTables,
} from "@/db/types/db";
import { DefaultHarnessProviderKind } from "@/src/dict/harness-provider-dict";
import grabProviderPreset from "@/src/harness/providers/grab-provider-preset";
/**
* Adds the default provider (OpenRouter) when no provider exists, so the
* free models work as soon as a key is set. Idempotent; a super admin who
* removed it and added others doesn't get it back.
*/
export default async function ensureDefaultHarnessProvider() {
const existing = await BunSQLite.sql<BUN_SQLITE_COBBLE_HARNESS_PROVIDERS>({
sql: `SELECT id FROM harness_providers LIMIT 1`,
});
if (existing.payload?.[0]) return;
const preset = grabProviderPreset({ kind: DefaultHarnessProviderKind });
const res = await BunSQLite.insert<
BUN_SQLITE_COBBLE_HARNESS_PROVIDERS,
(typeof BunSQLiteTables)[number]
>({
table: "harness_providers",
data: [
{
kind: preset.value,
name: preset.title,
base_url: preset.base_url,
enabled: 1,
},
],
});
if (!res.success) {
throw new Error(`Couldn't add the default model provider: ${res.msg}`);
}
}
@@ -0,0 +1,61 @@
import { Harnesses } from "@/src/dict/harness-dict";
import type { BotHarnessOverviewPayload } from "@/src/types";
import grabHarnessModelEntries from "./grab-harness-model-entries";
import grabHarnessOverviewItem from "./grab-harness-overview-item";
import grabHarnessSetting from "./grab-harness-setting";
type Params = {
/**
* A worker bot the caller has checked belongs to the session user.
*/
bot_id: number;
};
/**
* A worker bot's model settings page: per harness, the bot's own setting
* (or the default it inherits), the models it can pick, what it runs on
* now, and what the default runs on.
*/
export default async function grabBotHarnessOverview({
bot_id,
}: Params): Promise<BotHarnessOverviewPayload> {
const [own, defaults] = await Promise.all([
Promise.all(
Harnesses.map((harness) => grabHarnessSetting({ harness: harness.value, bot_id })),
),
Promise.all(
Harnesses.map((harness) => grabHarnessSetting({ harness: harness.value })),
),
]);
const entries = await grabHarnessModelEntries({
pinned_ids: [...own, ...defaults].flatMap((setting) =>
setting.model_id ? [setting.model_id] : [],
),
});
return {
harnesses: await Promise.all(
own.map(async (setting, index) => {
const [item, default_item] = await Promise.all([
grabHarnessOverviewItem({ harness: setting.harness, setting, entries, bot_id }),
grabHarnessOverviewItem({
harness: setting.harness,
setting: defaults[index]!,
entries,
}),
]);
return {
...item,
inherit: setting.source == "default",
default: {
enabled: default_item.enabled,
current: default_item.current,
candidate_count: default_item.candidate_count,
},
};
}),
),
};
}
@@ -0,0 +1,19 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_HARNESS_PROVIDERS } from "@/db/types/db";
import { DefaultHarnessProviderKind } from "@/src/dict/harness-provider-dict";
/**
* The default provider (`DefaultHarnessProviderKind`, OpenRouter): the
* oldest enabled one of that kind. Undefined once it's removed or turned
* off, which leaves harnesses on any provider.
*/
export default async function grabDefaultHarnessProviderId() {
const row = (
await BunSQLite.sql<BUN_SQLITE_COBBLE_HARNESS_PROVIDERS>({
sql: `SELECT id FROM harness_providers WHERE kind = ? AND enabled = 1 ORDER BY id ASC LIMIT 1`,
values: [DefaultHarnessProviderKind],
})
).payload?.[0];
return row?.id || undefined;
}
@@ -0,0 +1,60 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_HARNESS_MODELS } from "@/db/types/db";
import type { HarnessCatalogModel } from "@/src/harness/types";
import type { HarnessModelOption } from "@/src/types";
import harnessModelRowToCatalog from "./harness-model-row-to-catalog";
type Params = {
/**
* Picked models, listed even when no longer available.
*/
pinned_ids?: number[];
};
type ModelRow = BUN_SQLITE_COBBLE_HARNESS_MODELS & { provider_name?: string };
export type HarnessModelEntry = {
row: ModelRow;
model: HarnessCatalogModel;
option: HarnessModelOption;
};
/**
* Every model a setting can pick (available, on an enabled provider),
* free first, plus the given picked ones, each with its picker option.
*/
export default async function grabHarnessModelEntries({
pinned_ids = [],
}: Params = {}): Promise<HarnessModelEntry[]> {
const ids = [...new Set(pinned_ids)];
const rows =
(
await BunSQLite.sql<ModelRow>({
sql: `SELECT m.*, p.name AS provider_name FROM harness_models m
JOIN harness_providers p ON p.id = m.provider_id
WHERE (m.available = 1 AND p.enabled = 1)
${ids.length ? `OR m.id IN (${ids.map(() => "?").join(", ")})` : ""}
ORDER BY m.free DESC, p.id ASC, m.name ASC`,
values: ids,
})
).payload || [];
return rows.map((row) => {
const model = harnessModelRowToCatalog({ row });
return {
row,
model,
option: {
id: row.id || 0,
provider_id: row.provider_id || 0,
provider_name: row.provider_name || "",
slug: model.slug,
name: `${model.name || model.slug}${row.available == 0 ? " (no longer listed)" : ""}`,
free: model.free,
voices: model.voices,
},
};
});
}
@@ -0,0 +1,66 @@
import type { HarnessKind } from "@/src/dict/harness-dict";
import { Harnesses } from "@/src/dict/harness-dict";
import modelServesHarness from "@/src/harness/catalog/model-serves-harness";
import resolveHarnessCandidates from "@/src/harness/resolve/resolve-harness-candidates";
import type { HarnessOverview } from "@/src/types";
import type { HarnessModelEntry } from "./grab-harness-model-entries";
import type { HarnessSetting } from "./grab-harness-setting";
type Params = {
harness: HarnessKind;
setting: HarnessSetting;
entries: HarnessModelEntry[];
/**
* Resolve what runs now for this bot instead of the default.
*/
bot_id?: number | string;
};
/**
* One harness as the model settings pages show it: its setting, the
* models it can be set to, and the model it would use right now.
*/
export default async function grabHarnessOverviewItem({
harness,
setting,
entries,
bot_id,
}: Params): Promise<HarnessOverview> {
const meta = Harnesses.find((h) => h.value == harness)!;
const candidates = setting.enabled
? await resolveHarnessCandidates({ harness, bot_id }).catch(() => [])
: [];
const first = candidates[0];
const serving = entries.filter(({ model }) => modelServesHarness({ model, harness }));
const providers = [
...new Map(
serving.map(({ option }) => [
option.provider_id,
{ id: option.provider_id, name: option.provider_name },
]),
).values(),
];
if (setting.provider_id && !providers.some((p) => p.id == setting.provider_id)) {
providers.push({ id: setting.provider_id, name: "Unavailable provider" });
}
const current = first
? entries.find(({ row, model }) => `${row.provider_id}:${model.slug}` == first.key)
: undefined;
return {
harness,
title: meta.title,
description: meta.description,
required: meta.required,
model_id: setting.model_id,
provider_id: setting.provider_id,
providers,
fallback: setting.fallback,
enabled: setting.enabled,
options: setting.options,
models: serving.map(({ option }) => option),
current: current && first ? { ...current.option, source: first.source } : undefined,
candidate_count: candidates.length,
};
}
@@ -0,0 +1,33 @@
import { Harnesses } from "@/src/dict/harness-dict";
import type { HarnessOverviewPayload } from "@/src/types";
import grabHarnessModelEntries from "./grab-harness-model-entries";
import grabHarnessOverviewItem from "./grab-harness-overview-item";
import grabHarnessProviders from "./grab-harness-providers";
import grabHarnessSetting from "./grab-harness-setting";
import toPublicHarnessProvider from "./to-public-harness-provider";
/**
* Everything the Models settings page shows: the providers (without keys)
* and, per harness, its default setting, the models it can be set to
* (free first) and what it runs on right now.
*/
export default async function grabHarnessOverview(): Promise<HarnessOverviewPayload> {
const providers = await grabHarnessProviders();
const settings = await Promise.all(
Harnesses.map((harness) => grabHarnessSetting({ harness: harness.value })),
);
const entries = await grabHarnessModelEntries({
pinned_ids: settings.flatMap((setting) =>
setting.model_id ? [setting.model_id] : [],
),
});
return {
providers: providers.map((provider) => toPublicHarnessProvider({ provider })),
harnesses: await Promise.all(
settings.map((setting) =>
grabHarnessOverviewItem({ harness: setting.harness, setting, entries }),
),
),
};
}
@@ -0,0 +1,35 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_HARNESS_PROVIDERS } from "@/db/types/db";
type Params = {
provider_id?: number | string;
enabled_only?: boolean;
};
/**
* Provider rows, oldest first. They hold the encrypted key: server only,
* never hand them to the client as they are (`toPublicHarnessProvider`).
*/
export default async function grabHarnessProviders({
provider_id,
enabled_only,
}: Params = {}) {
const where: string[] = [];
const values: (string | number)[] = [];
if (provider_id) {
where.push(`id = ?`);
values.push(provider_id);
}
if (enabled_only) {
where.push(`enabled = 1`);
}
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_HARNESS_PROVIDERS>({
sql: `SELECT * FROM harness_providers ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY id ASC`,
values,
});
return res.payload || [];
}
@@ -0,0 +1,113 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type {
BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS,
BUN_SQLITE_COBBLE_HARNESS_SETTINGS,
} from "@/db/types/db";
import type { HarnessKind } from "@/src/dict/harness-dict";
import parseJobJson from "@/src/bots-queue/parse-job-json";
import grabDefaultHarnessProviderId from "./grab-default-harness-provider-id";
export type HarnessSetting = {
harness: HarnessKind;
/**
* The picked `harness_models` row; undefined picks automatically.
*/
model_id?: number;
/**
* The `harness_providers` row the harness is limited to (its pick
* and its automatic picks); undefined: any provider.
*/
provider_id?: number;
fallback: boolean;
enabled: boolean;
options: Record<string, unknown>;
/**
* `bot` when a worker bot overrides the default for this harness.
*/
source: "default" | "bot";
};
type Params = {
harness: HarnessKind;
/**
* The bot the call is for. A worker bot's own setting wins; the
* coordinator always uses the default.
*/
bot_id?: number | string;
};
type SettingRow = Pick<
BUN_SQLITE_COBBLE_HARNESS_SETTINGS,
"model_id" | "provider_id" | "fallback" | "enabled" | "options"
>;
/**
* A harness's setting for a call. With no row it is automatic on the
* default provider (OpenRouter), on, with fallback. Text is always on.
*/
export default async function grabHarnessSetting({
harness,
bot_id,
}: Params): Promise<HarnessSetting> {
const bot_row = bot_id
? await grabBotOverride({ harness, bot_id })
: undefined;
if (bot_row) {
return toSetting({ harness, row: bot_row, source: "bot" });
}
const row = (
await BunSQLite.sql<BUN_SQLITE_COBBLE_HARNESS_SETTINGS>({
sql: `SELECT * FROM harness_settings WHERE harness = ? LIMIT 1`,
values: [harness],
})
).payload?.[0];
if (!row) {
return {
...toSetting({ harness, source: "default" }),
provider_id: await grabDefaultHarnessProviderId(),
};
}
return toSetting({ harness, row, source: "default" });
}
async function grabBotOverride({
harness,
bot_id,
}: {
harness: HarnessKind;
bot_id: number | string;
}) {
return (
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS>({
sql: `SELECT s.* FROM bot_harness_settings s
JOIN bots b ON b.id = s.bot_id
WHERE s.bot_id = ? AND s.harness = ? AND b.kind != 'coordinator'
LIMIT 1`,
values: [bot_id, harness],
})
).payload?.[0];
}
function toSetting({
harness,
row,
source,
}: {
harness: HarnessKind;
row?: SettingRow;
source: HarnessSetting["source"];
}): HarnessSetting {
return {
harness,
model_id: typeof row?.model_id == "number" ? row.model_id : undefined,
provider_id: typeof row?.provider_id == "number" ? row.provider_id : undefined,
fallback: row ? row.fallback != 0 : true,
enabled: harness == "text" || !row || row.enabled != 0,
options: parseJobJson({ text: row?.options }) || {},
source,
};
}
@@ -0,0 +1,46 @@
import type { BUN_SQLITE_COBBLE_HARNESS_MODELS } from "@/db/types/db";
import type { HarnessCatalogModel } from "@/src/harness/types";
type Params = {
row: BUN_SQLITE_COBBLE_HARNESS_MODELS;
};
/**
* A `harness_models` row as a catalog entry.
*/
export default function harnessModelRowToCatalog({
row,
}: Params): HarnessCatalogModel {
return {
slug: row.slug || "",
name: row.name || row.slug || "",
free: row.free == 1,
input_modalities: parseList({ text: row.input_modalities }) || ["text"],
output_modalities: parseList({ text: row.output_modalities }) || [
"text",
],
supports_tools: row.supports_tools == 1,
context_length: numberOrUndefined(row.context_length),
max_output_tokens: numberOrUndefined(row.max_output_tokens),
voices: parseList({ text: row.voices }),
intelligence_score: numberOrUndefined(row.intelligence_score),
agentic_score: numberOrUndefined(row.agentic_score),
released_at: numberOrUndefined(row.released_at),
expires_at: numberOrUndefined(row.expires_at),
};
}
function parseList({ text }: { text?: string | null }) {
if (!text) return undefined;
try {
const value = JSON.parse(text);
return Array.isArray(value) ? value.map(String) : undefined;
} catch {
return undefined;
}
}
function numberOrUndefined(value: unknown) {
return typeof value == "number" ? value : undefined;
}
@@ -0,0 +1,44 @@
import type { BUN_SQLITE_COBBLE_HARNESS_PROVIDERS } from "@/db/types/db";
import type { HarnessProviderKind } from "@/src/dict/harness-provider-dict";
import grabProviderPreset from "@/src/harness/providers/grab-provider-preset";
import type { HarnessProviderConfig } from "@/src/harness/types";
import { decrypt } from "@/src/utils/crypt";
import grabEnv from "../envs/grab-env";
type Params = {
provider: BUN_SQLITE_COBBLE_HARNESS_PROVIDERS;
};
/**
* A provider row as the adapters need it, with its key decrypted (or, if
* none is saved, the preset's env: set in Settings, under Environment, or
* the process environment, `grabEnv`). `usable` is
* false when the provider needs a key and has none.
*/
export default async function resolveProviderConfig({
provider,
}: Params): Promise<{ config: HarnessProviderConfig; usable: boolean }> {
const preset = grabProviderPreset({ kind: provider.kind });
let api_key: string | undefined;
if (provider.api_key) {
api_key = await decrypt(provider.api_key).catch(() => undefined);
}
if (!api_key && preset.key_env) {
api_key = await grabEnv({ key: preset.key_env });
}
const config: HarnessProviderConfig = {
id: provider.id || undefined,
kind: preset.value as HarnessProviderKind,
name: provider.name || preset.title,
base_url: provider.base_url || preset.base_url,
api_key,
};
return {
config,
usable: Boolean(config.base_url) && (!preset.needs_key || !!api_key),
};
}
@@ -0,0 +1,86 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { HarnessKind } from "@/src/dict/harness-dict";
import AppError from "@/src/utils/app-error";
import assertHarnessModel from "./assert-harness-model";
import assertHarnessProvider from "./assert-harness-provider";
type Params = {
/**
* A worker bot the caller has checked belongs to the session user.
*/
bot_id: number;
harness: HarnessKind;
/**
* Drop the bot's own setting and use the default again.
*/
inherit: boolean;
model_id: number | null;
/**
* Limits the harness to one provider; null: any.
*/
provider_id?: number | null;
fallback: boolean;
enabled: boolean;
options?: Record<string, unknown>;
};
/**
* Saves (or, with `inherit`, removes) a worker bot's own setting for one
* harness. Same rules as the default: the model must be available and fit
* the job, and text can't be turned off.
*/
export default async function saveBotHarnessSetting({
bot_id,
harness,
inherit,
model_id,
provider_id = null,
fallback,
enabled,
options,
}: Params) {
if (inherit) {
await BunSQLite.sql({
sql: `DELETE FROM bot_harness_settings WHERE bot_id = ? AND harness = ?`,
values: [bot_id, harness],
});
return;
}
if (harness == "text" && !enabled) {
throw new AppError(`The text model can't be turned off.`);
}
if (provider_id) {
await assertHarnessProvider({ provider_id });
}
if (model_id) {
await assertHarnessModel({ harness, model_id, provider_id });
}
const now = Date.now();
const res = await BunSQLite.sql({
sql: `INSERT INTO bot_harness_settings (created_at, updated_at, bot_id, harness, model_id, provider_id, fallback, enabled, options)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(bot_id, harness) DO UPDATE SET updated_at = excluded.updated_at,
model_id = excluded.model_id, provider_id = excluded.provider_id, fallback = excluded.fallback,
enabled = excluded.enabled, options = excluded.options`,
values: [
now,
now,
bot_id,
harness,
model_id,
provider_id,
fallback ? 1 : 0,
enabled ? 1 : 0,
JSON.stringify(options || {}),
],
});
if (!res.success) {
throw new Error(`Couldn't save the bot's model setting: ${res.error}`);
}
}
@@ -0,0 +1,102 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type {
BUN_SQLITE_COBBLE_HARNESS_PROVIDERS,
BunSQLiteTables,
} from "@/db/types/db";
import type { HarnessProviderKind } from "@/src/dict/harness-provider-dict";
import grabProviderPreset from "@/src/harness/providers/grab-provider-preset";
import AppError from "@/src/utils/app-error";
import { encrypt } from "@/src/utils/crypt";
import grabHarnessProviders from "./grab-harness-providers";
type Params = {
/**
* Updates this provider; adds one when not given.
*/
provider_id?: number;
kind: HarnessProviderKind;
name?: string;
base_url?: string;
/**
* A new key. Left out keeps the saved one.
*/
api_key?: string;
clear_key?: boolean;
enabled: boolean;
};
/**
* Adds or updates a model provider. The key is encrypted before it's
* stored and never read back to the client. Returns the provider row.
*/
export default async function saveHarnessProvider({
provider_id,
kind,
name,
base_url,
api_key,
clear_key,
enabled,
}: Params) {
const preset = grabProviderPreset({ kind });
const final_base_url = (base_url || preset.base_url).trim().replace(/\/+$/, "");
if (!/^https?:\/\/[^\s/]+/i.test(final_base_url)) {
throw new AppError(`Enter the provider's API address, starting with http:// or https://.`);
}
const existing = provider_id
? (await grabHarnessProviders({ provider_id }))[0]
: undefined;
if (provider_id && !existing) {
throw new AppError(`Provider not found.`);
}
const key_update = api_key?.trim()
? { api_key: await encrypt(api_key.trim()) }
: clear_key
? { api_key: "" }
: {};
const data: BUN_SQLITE_COBBLE_HARNESS_PROVIDERS = {
kind: preset.value,
name: name?.trim() || preset.title,
base_url: final_base_url,
enabled: enabled ? 1 : 0,
...key_update,
};
if (existing?.id) {
const res = await BunSQLite.update<
BUN_SQLITE_COBBLE_HARNESS_PROVIDERS,
(typeof BunSQLiteTables)[number]
>({
table: "harness_providers",
data,
targetId: existing.id,
});
if (!res.success) {
throw new Error(`Couldn't save the provider: ${res.msg}`);
}
return (await grabHarnessProviders({ provider_id: existing.id }))[0]!;
}
const res = await BunSQLite.insert<
BUN_SQLITE_COBBLE_HARNESS_PROVIDERS,
(typeof BunSQLiteTables)[number]
>({
table: "harness_providers",
data: [data],
});
const new_id = res.singleRes?.id || res.postInsertReturn?.insertId;
if (!res.success || !new_id) {
throw new Error(`Couldn't add the provider: ${res.msg}`);
}
return (await grabHarnessProviders({ provider_id: new_id }))[0]!;
}
@@ -0,0 +1,69 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { HarnessKind } from "@/src/dict/harness-dict";
import AppError from "@/src/utils/app-error";
import assertHarnessModel from "./assert-harness-model";
import assertHarnessProvider from "./assert-harness-provider";
type Params = {
harness: HarnessKind;
/**
* A `harness_models` row, or null to pick automatically.
*/
model_id: number | null;
/**
* Limits the harness to one provider; null: any.
*/
provider_id?: number | null;
fallback: boolean;
enabled: boolean;
options?: Record<string, unknown>;
};
/**
* Saves a harness's model setting. The picked model must be available and
* able to do the harness's job. Text can't be turned off.
*/
export default async function saveHarnessSetting({
harness,
model_id,
provider_id = null,
fallback,
enabled,
options,
}: Params) {
if (harness == "text" && !enabled) {
throw new AppError(`The text model can't be turned off.`);
}
if (provider_id) {
await assertHarnessProvider({ provider_id });
}
if (model_id) {
await assertHarnessModel({ harness, model_id, provider_id });
}
const now = Date.now();
const res = await BunSQLite.sql({
sql: `INSERT INTO harness_settings (created_at, updated_at, harness, model_id, provider_id, fallback, enabled, options)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(harness) DO UPDATE SET updated_at = excluded.updated_at,
model_id = excluded.model_id, provider_id = excluded.provider_id, fallback = excluded.fallback,
enabled = excluded.enabled, options = excluded.options`,
values: [
now,
now,
harness,
model_id,
provider_id,
fallback ? 1 : 0,
enabled ? 1 : 0,
JSON.stringify(options || {}),
],
});
if (!res.success) {
throw new Error(`Couldn't save the model setting: ${res.error}`);
}
}
@@ -0,0 +1,30 @@
import ensureDefaultHarnessProvider from "./ensure-default-harness-provider";
import grabHarnessProviders from "./grab-harness-providers";
import syncHarnessProviderModels from "./sync-harness-provider-models";
/**
* Refreshes every enabled provider's model list, so automatic picks follow
* free models as they come and go. One provider failing doesn't stop the
* rest; its error is saved on its row.
*/
export default async function syncAllHarnessModels() {
await ensureDefaultHarnessProvider();
const providers = await grabHarnessProviders({ enabled_only: true });
const results: { provider_id?: number; count?: number; error?: string }[] =
[];
for (const provider of providers) {
try {
const count = await syncHarnessProviderModels({ provider });
results.push({ provider_id: provider.id || undefined, count });
} catch (error: unknown) {
results.push({
provider_id: provider.id || undefined,
error: error instanceof Error ? error.message : String(error),
});
}
}
return results;
}
@@ -0,0 +1,54 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_HARNESS_PROVIDERS } from "@/db/types/db";
import grabHarnessAdapter from "@/src/harness/providers/grab-harness-adapter";
import resolveProviderConfig from "./resolve-provider-config";
import upsertHarnessModels from "./upsert-harness-models";
type Params = {
provider: BUN_SQLITE_COBBLE_HARNESS_PROVIDERS;
};
/**
* Fetches one provider's model list into `harness_models`, and records
* when it ran and why it failed, if it did. Returns how many models the
* provider listed. Throws when the list couldn't be fetched.
*/
export default async function syncHarnessProviderModels({ provider }: Params) {
if (!provider.id) {
throw new Error(`Provider has no id`);
}
const { config } = await resolveProviderConfig({ provider });
try {
const models = await grabHarnessAdapter({
kind: config.kind,
}).listModels({ provider: config });
const count = await upsertHarnessModels({
provider_id: provider.id,
models,
});
await BunSQLite.sql({
sql: `UPDATE harness_providers SET last_synced_at = ?, sync_error = NULL, updated_at = ? WHERE id = ?`,
values: [Date.now(), Date.now(), provider.id],
});
return count;
} catch (error: unknown) {
await BunSQLite.sql({
sql: `UPDATE harness_providers SET sync_error = ?, updated_at = ? WHERE id = ?`,
values: [
(error instanceof Error ? error.message : String(error)).slice(
0,
500,
),
Date.now(),
provider.id,
],
});
throw error;
}
}
@@ -0,0 +1,31 @@
import type { BUN_SQLITE_COBBLE_HARNESS_PROVIDERS } from "@/db/types/db";
import grabProviderPreset from "@/src/harness/providers/grab-provider-preset";
import type { PublicHarnessProvider } from "@/src/types";
type Params = {
provider: BUN_SQLITE_COBBLE_HARNESS_PROVIDERS;
};
/**
* A provider row safe for the client: the key is dropped, only whether
* one is saved (or set in the environment) is kept.
*/
export default function toPublicHarnessProvider({
provider,
}: Params): PublicHarnessProvider {
const preset = grabProviderPreset({ kind: provider.kind });
return {
id: provider.id || 0,
kind: preset.value,
name: provider.name || preset.title,
base_url: provider.base_url || preset.base_url,
enabled: provider.enabled != 0,
has_key: Boolean(provider.api_key),
has_env_key: Boolean(preset.key_env && process.env[preset.key_env]),
needs_key: preset.needs_key,
key_env: preset.key_env || undefined,
last_synced_at: provider.last_synced_at || undefined,
sync_error: provider.sync_error || undefined,
};
}
@@ -0,0 +1,92 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { HarnessCatalogModel } from "@/src/harness/types";
type Params = {
provider_id: number;
models: HarnessCatalogModel[];
now?: number;
};
const Columns = [
"created_at",
"updated_at",
"provider_id",
"slug",
"name",
"free",
"available",
"input_modalities",
"output_modalities",
"supports_tools",
"context_length",
"max_output_tokens",
"voices",
"intelligence_score",
"agentic_score",
"released_at",
"expires_at",
"last_seen_at",
] as const;
/**
* Rows per statement, well under SQLite's bound-variable limit.
*/
const ChunkSize = 50;
/**
* Writes a provider's model list: new models are added, listed ones are
* refreshed and marked available, and models it no longer lists are kept
* but marked unavailable (a super admin's pick may point at them).
*/
export default async function upsertHarnessModels({
provider_id,
models,
now = Date.now(),
}: Params) {
const unique = [...new Map(models.map((m) => [m.slug, m])).values()];
for (let i = 0; i < unique.length; i += ChunkSize) {
const chunk = unique.slice(i, i + ChunkSize);
const res = await BunSQLite.sql({
sql: `INSERT INTO harness_models (${Columns.join(", ")})
VALUES ${chunk.map(() => `(${Columns.map(() => "?").join(", ")})`).join(", ")}
ON CONFLICT(provider_id, slug) DO UPDATE SET
${Columns.filter((c) => c != "created_at" && c != "provider_id" && c != "slug")
.map((c) => `${c} = excluded.${c}`)
.join(", ")}`,
values: chunk.flatMap((model) => [
now,
now,
provider_id,
model.slug,
model.name || model.slug,
model.free ? 1 : 0,
1,
JSON.stringify(model.input_modalities),
JSON.stringify(model.output_modalities),
model.supports_tools ? 1 : 0,
model.context_length ?? null,
model.max_output_tokens ?? null,
model.voices ? JSON.stringify(model.voices) : null,
model.intelligence_score ?? null,
model.agentic_score ?? null,
model.released_at ?? null,
model.expires_at ?? null,
now,
]),
});
if (!res.success) {
throw new Error(`Couldn't save the model list: ${res.error}`);
}
}
await BunSQLite.sql({
sql: `UPDATE harness_models SET available = 0, updated_at = ?
WHERE provider_id = ? AND available = 1 AND (last_seen_at IS NULL OR last_seen_at < ?)`,
values: [now, provider_id, now],
});
return unique.length;
}
@@ -20,7 +20,7 @@ export default async function MediaDataToArrayBuffer(
if (body instanceof ReadableStream) {
return Bun.readableStreamToArrayBuffer(body);
}
// Blob, BunFile, Request, Response — wrap in Response and use Bun's reader
// Blob, BunFile, Request, Response: wrap in Response and use Bun's reader
return new Response(body as BodyInit)
.bytes()
.then((b) => b.buffer as ArrayBuffer);
@@ -230,7 +230,13 @@ export default async function uploadAndRecordMedia({
const media_data: BUN_SQLITE_COBBLE_MEDIA = {
...media,
user_id: Number(user.id),
is_primary: is_primary ? 1 : 0,
/**
* NULL, not 0, when not primary: the (user_id, is_primary) unique
* constraint only means one primary image per user, and SQLite
* lets NULLs repeat.
*/
is_primary: is_primary ? 1 : (null as any),
is_private: is_private ? 1 : 0,
media_write_path: media_write_path,
media_thumbnail_write_path:
media_type === "image" ? media_thumbnail_write_path : undefined,