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:
@@ -91,7 +91,7 @@ export default async function grabBotChatTools({
|
||||
}),
|
||||
...(computers.length
|
||||
? [
|
||||
searchComputersTool({ computers, workspace_id }),
|
||||
searchComputersTool({ bot_id, computers, workspace_id }),
|
||||
askToTakeOverTool({ computers, onAsk: (params) => onTakeOver?.(params) }),
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -63,7 +63,7 @@ export default async function openComputerTools({
|
||||
throw new Error(computerNotReadyMessage(target));
|
||||
}
|
||||
|
||||
const envs = await grabComputerEnvs({ computer_id, workspace_id });
|
||||
const envs = await grabComputerEnvs({ computer_id, bot_id, workspace_id });
|
||||
const paths = await writeComputerEnv({ base_url: target.base_url, envs, signal });
|
||||
const secrets = envs.filter((env) => env.secret);
|
||||
const redact = (text: string) => redactSecrets({ text, secrets });
|
||||
|
||||
@@ -10,6 +10,10 @@ import type { HarnessTool } from "../../types";
|
||||
import runComputerCommand from "./run-computer-command";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The bot the run is for: its own envs beat everything.
|
||||
*/
|
||||
bot_id?: number | string | null;
|
||||
/**
|
||||
* The workspace the run is in: its envs apply.
|
||||
*/
|
||||
@@ -31,6 +35,7 @@ type Args = {
|
||||
* so the bot can tell the user exactly which computer holds what.
|
||||
*/
|
||||
export default function searchComputersTool({
|
||||
bot_id,
|
||||
workspace_id,
|
||||
computers,
|
||||
}: Params): HarnessTool<Args> {
|
||||
@@ -59,7 +64,11 @@ export default function searchComputersTool({
|
||||
timeout_ms: AppData.ComputerSearchTimeoutMs,
|
||||
signal,
|
||||
}),
|
||||
grabComputerEnvs({ computer_id: computer.id || 0, workspace_id }),
|
||||
grabComputerEnvs({
|
||||
computer_id: computer.id || 0,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}),
|
||||
]);
|
||||
|
||||
const output = redactSecrets({
|
||||
|
||||
@@ -52,7 +52,14 @@ export default function buildCustomTool({
|
||||
case "http":
|
||||
return runHttpTool({ config: tool.config, args, envs, signal });
|
||||
case "script":
|
||||
return runScriptTool({ config: tool.config, args, computer_id: computer_id!, workspace_id, signal });
|
||||
return runScriptTool({
|
||||
config: tool.config,
|
||||
args,
|
||||
computer_id: computer_id!,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
signal,
|
||||
});
|
||||
case "prompt":
|
||||
return runPromptTool({ config: tool.config, args, envs, bot_id, workspace_id, signal });
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ type Params = {
|
||||
config: CustomToolScriptConfig;
|
||||
args: Record<string, unknown>;
|
||||
computer_id: number | string;
|
||||
/**
|
||||
* The bot the run is for: its own envs beat everything.
|
||||
*/
|
||||
bot_id?: number | string | null;
|
||||
/**
|
||||
* The workspace the run is in: its envs apply on the computer.
|
||||
*/
|
||||
@@ -26,13 +30,20 @@ type Params = {
|
||||
* so nothing in them needs quoting. Returns what it printed (stdout and
|
||||
* stderr), secrets redacted; a non-zero exit throws with it.
|
||||
*/
|
||||
export default async function runScriptTool({ config, args, computer_id, workspace_id, signal }: Params) {
|
||||
export default async function runScriptTool({
|
||||
config,
|
||||
args,
|
||||
computer_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
signal,
|
||||
}: Params) {
|
||||
const target = await grabComputerMcpUrl({ computer_id });
|
||||
|
||||
if (!target) throw new Error(`Computer #${computer_id} has no container to run on.`);
|
||||
if (!target.ready) throw new Error(computerNotReadyMessage(target));
|
||||
|
||||
const envs = await grabComputerEnvs({ computer_id, workspace_id });
|
||||
const envs = await grabComputerEnvs({ computer_id, bot_id, workspace_id });
|
||||
const { shell_path } = await writeComputerEnv({ base_url: target.base_url, envs, signal });
|
||||
const language =
|
||||
CustomToolScriptLanguages.find((l) => l.value == config.language) || CustomToolScriptLanguages[0];
|
||||
|
||||
@@ -51,7 +51,7 @@ export default async function openBotExtensions({
|
||||
|
||||
if (!servers.length && !tools.length) return { tools: [], note: "", close: async () => {} };
|
||||
|
||||
const envs = await grabRunEnvs({ workspace_id });
|
||||
const envs = await grabRunEnvs({ bot_id, workspace_id });
|
||||
const secrets = envs.filter((env) => env.secret);
|
||||
const redact = (text: string) => redactSecrets({ text, secrets });
|
||||
const names = new Set(taken);
|
||||
|
||||
Reference in New Issue
Block a user