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
@@ -4,6 +4,10 @@ import resolveScopedEnvs from "./resolve-scoped-envs";
type Params = {
computer_id: number | string;
/**
* The bot the run is for: its own envs beat everything else.
*/
bot_id?: number | string | null;
/**
* The workspace the run is in. Its envs apply when the computer is
* linked to it (and it isn't archived); outside a workspace only the
@@ -15,7 +19,7 @@ type Params = {
/**
* The envs a run on this computer gets (`resolveScopedEnvs`).
*/
export default async function grabComputerEnvs({ computer_id, workspace_id }: Params) {
export default async function grabComputerEnvs({ computer_id, bot_id, workspace_id }: Params) {
const linked = workspace_id
? (
await BunSQLite.sql<{ id: number }>({
@@ -29,6 +33,7 @@ export default async function grabComputerEnvs({ computer_id, workspace_id }: Pa
return resolveScopedEnvs({
envs: await grabAllEnvs(),
bot_id,
computer_id: Number(computer_id),
workspace_ids: linked?.id ? [Number(linked.id)] : [],
});