Let a bot and a workspace set their own envs

Envs had three scopes, general, workspace and computer. A bot had no
scope of its own, so the only way to give one worker a key was to set it
on a computer it happened to share.

The envs table gets a bot_id, and resolution now layers bot over
computer over workspace over general, so a bot's own key wins wherever it
runs. Every computer call site passes the run's bot_id, and grabRunEnvs
does the same for the {{env.KEY}} a custom tool or an MCP server's headers
read.

A bot's and a workspace's settings page can now edit their own envs, in
place, through the same save-env and delete-env routes: sending `scoped`
makes them answer with just that scope instead of the whole install's
overview. Super admins only, like the Environment page.
This commit is contained in:
2026-10-22 08:30:40 +01:00
parent 7a3d2df213
commit 96735de48f
38 changed files with 551 additions and 135 deletions
+32 -7
View File
@@ -3,9 +3,10 @@ import type { BUN_SQLITE_COBBLE_ENVS } from "@/db/types/db";
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import deleteEnv from "@/src/functions/backend/db/envs/delete-env";
import grabEnvsOverview from "@/src/functions/backend/db/envs/grab-envs-overview";
import grabScopedEnvs from "@/src/functions/backend/db/envs/grab-scoped-envs";
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
import { EnvIdRequestSchema } from "@/src/schemas/env-schema";
import type { EnvsOverviewPayload } from "@/src/types";
import type { EnvsMutationResult } from "@/src/types";
import AppError from "@/src/utils/app-error";
import parseWithSchema from "@/src/utils/parse-with-schema";
import safeErrorMessage from "@/src/utils/safe-error-message";
@@ -15,10 +16,12 @@ import type {
} from "@moduletrace/bunext/types";
/**
* Super admin only: removes an env.
* Super admin only: removes an env. Answers with the fresh page data, the
* Environment overview or, from a bot's or workspace's settings page
* (`scoped`), just that scope's envs.
*/
export const handler: BunextAPIRouteHandler<
APIResponseObject<EnvsOverviewPayload>
APIResponseObject<EnvsMutationResult>
> = async ({ req, body }) => {
try {
if (req.method != "POST") {
@@ -31,11 +34,14 @@ export const handler: BunextAPIRouteHandler<
throw new AppError(`Only a super admin can change the environment.`);
}
const { env_id } = parseWithSchema({ schema: EnvIdRequestSchema, data: body });
const { env_id, scoped } = parseWithSchema({
schema: EnvIdRequestSchema,
data: body,
});
const env = (
await BunSQLite.sql<BUN_SQLITE_COBBLE_ENVS>({
sql: `SELECT key, paradigm FROM envs WHERE id = ? LIMIT 1`,
sql: `SELECT key, paradigm, workspace_id, bot_id FROM envs WHERE id = ? LIMIT 1`,
values: [env_id],
})
).payload?.[0];
@@ -49,14 +55,33 @@ export const handler: BunextAPIRouteHandler<
category: "system",
user_id: user.id,
plain_text: `User #${user.id} removed ${env.key} (${env.paradigm}).`,
metadata: { env_id, key: env.key },
metadata: { env_id, key: env.key, paradigm: env.paradigm },
ip_address: grabRequestIP(req),
});
if (scoped && (env.paradigm == "bot" || env.paradigm == "workspace")) {
const owner_id = env.paradigm == "bot" ? env.bot_id : env.workspace_id;
if (owner_id) {
return {
success: true,
msg: `Removed ${env.key}.`,
singleRes: {
scoped: await grabScopedEnvs({
paradigm: env.paradigm,
owner_id,
user_id: Number(user.id),
can_manage: true,
}),
},
};
}
}
return {
success: true,
msg: `Removed ${env.key}.`,
singleRes: await grabEnvsOverview({ user_id: Number(user.id) }),
singleRes: { overview: await grabEnvsOverview({ user_id: Number(user.id) }) },
};
} catch (error: unknown) {
return { success: false, msg: safeErrorMessage({ error }) };
+55 -6
View File
@@ -1,9 +1,10 @@
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import grabEnvsOverview from "@/src/functions/backend/db/envs/grab-envs-overview";
import grabScopedEnvs from "@/src/functions/backend/db/envs/grab-scoped-envs";
import saveEnv from "@/src/functions/backend/db/envs/save-env";
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
import { SaveEnvRequestSchema } from "@/src/schemas/env-schema";
import type { EnvsOverviewPayload } from "@/src/types";
import { SaveEnvRequestSchema, type SaveEnvRequest } from "@/src/schemas/env-schema";
import type { EnvsMutationResult } from "@/src/types";
import AppError from "@/src/utils/app-error";
import parseWithSchema from "@/src/utils/parse-with-schema";
import safeErrorMessage from "@/src/utils/safe-error-message";
@@ -14,9 +15,12 @@ import type {
/**
* Super admin only: adds or edits an env. Logged by key, never by value.
*
* Answers with the fresh page data: the Environment overview or, from a
* bot's or workspace's settings page (`scoped`), just that scope's envs.
*/
export const handler: BunextAPIRouteHandler<
APIResponseObject<EnvsOverviewPayload>
APIResponseObject<EnvsMutationResult>
> = async ({ req, body }) => {
try {
if (req.method != "POST") {
@@ -33,21 +37,66 @@ export const handler: BunextAPIRouteHandler<
await saveEnv({ ...env, user_id: Number(user.id) });
const scope_id =
env.paradigm == "workspace"
? env.workspace_id
: env.paradigm == "bot"
? env.bot_id
: env.paradigm == "computer"
? env.computer_id
: null;
await logEvent({
title: env.env_id ? "Env changed" : "Env added",
category: "system",
user_id: user.id,
plain_text: `User #${user.id} ${env.env_id ? "changed" : "added"} ${env.key} (${env.paradigm}${env.workspace_id ? ` #${env.workspace_id}` : env.computer_id ? ` #${env.computer_id}` : ""}).`,
metadata: { key: env.key, paradigm: env.paradigm, workspace_id: env.workspace_id, computer_id: env.computer_id },
plain_text: `User #${user.id} ${env.env_id ? "changed" : "added"} ${env.key} (${env.paradigm}${scope_id ? ` #${scope_id}` : ""}).`,
metadata: {
key: env.key,
paradigm: env.paradigm,
workspace_id: env.workspace_id,
computer_id: env.computer_id,
bot_id: env.bot_id,
},
ip_address: grabRequestIP(req),
});
return {
success: true,
msg: `Saved ${env.key.trim()}.`,
singleRes: await grabEnvsOverview({ user_id: Number(user.id) }),
singleRes: await mutationResult({ env, scoped: env.scoped, user_id: Number(user.id) }),
};
} catch (error: unknown) {
return { success: false, msg: safeErrorMessage({ error }) };
}
};
/**
* What the page is showing after the change.
*/
async function mutationResult({
env,
scoped,
user_id,
}: {
env: SaveEnvRequest;
scoped?: boolean;
user_id: number;
}): Promise<EnvsMutationResult> {
if (scoped && (env.paradigm == "bot" || env.paradigm == "workspace")) {
const owner_id = env.paradigm == "bot" ? env.bot_id : env.workspace_id;
if (owner_id) {
return {
scoped: await grabScopedEnvs({
paradigm: env.paradigm,
owner_id,
user_id,
can_manage: true,
}),
};
}
}
return { overview: await grabEnvsOverview({ user_id }) };
}