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
@@ -2,6 +2,7 @@ import { useContext, useState } from "react";
import EmptyContent from "@moduletrace/twui/elements/EmptyContent";
import { AppContext } from "@/src/pages/__root";
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
import ScopedEnvsGroup from "../../../(partials)/envs/scoped-envs-group";
import ExtensionsGroup from "../../../(partials)/extensions-group";
import SettingsSectionHeading from "../../../(partials)/settings-section-heading";
import SettingsShell from "../../../(partials)/settings-shell";
@@ -70,6 +71,7 @@ export default function WorkspaceOverview() {
description="Every bot working here gets these, the coordinator too, on top of its own, only while it works here."
initial={pageProps?.workspace_extensions}
/>
<ScopedEnvsGroup initial={pageProps?.workspace_envs} />
{harnesses?.length ? (
<>
<SettingsSectionHeading
@@ -2,6 +2,9 @@ import type { PagePropsType, PageQueryObject } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
import grabBots from "@/src/functions/backend/db/bots/grab-bots";
import grabUserComputers from "@/src/functions/backend/db/computers/grab-user-computers";
import grabScopedEnvs from "@/src/functions/backend/db/envs/grab-scoped-envs";
import userAuth from "@/src/functions/backend/auth/user-auth";
import isPassThroughUser from "@/src/utils/rbac/is-pass-through-user";
import grabWorkspaceExtensionsOverview from "@/src/functions/backend/db/extensions/grab-workspace-extensions-overview";
import grabBotHarnessOverview from "@/src/functions/backend/db/harness/grab-bot-harness-overview";
import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-workspaces";
@@ -45,6 +48,7 @@ export const server: BunextPageServerFn<PagePropsType> = async (params) => {
computers,
workspace_harness_overview,
workspace_extensions,
workspace_envs,
] = await Promise.all([
grabWorkspaceBots({ user_id, workspace_id }),
grabBots({ user_id }),
@@ -52,6 +56,14 @@ export const server: BunextPageServerFn<PagePropsType> = async (params) => {
grabUserComputers({ user_id }),
grabBotHarnessOverview({ workspace_id }).catch(() => null),
grabWorkspaceExtensionsOverview({ user_id, workspace_id }),
grabScopedEnvs({
paradigm: "workspace",
owner_id: workspace_id,
user_id,
can_manage: isPassThroughUser({
user_types: (await userAuth({ req: params.req })).user_types,
}),
}),
]);
const crew = railBots({ bots: workspace_bots });
@@ -68,6 +80,7 @@ export const server: BunextPageServerFn<PagePropsType> = async (params) => {
computers,
workspace_harness_overview,
workspace_extensions,
workspace_envs,
},
};
};