Give bots skills: runbooks they load when a job matches

A skill is a runbook: a name, a unique slug, a one-line description of when
it applies, and the markdown body. A run only ever sees the slug and
description (one load_skill tool carries them), and reads the whole body
only when it calls the tool, so a long runbook costs nothing until needed.

Skills are the third extension kind beside MCP servers and custom tools, on
the same botExtensionsSql path, so per-bot, per-workspace and all-bots
grants work the same way, and attachDefaultExtensions hands an every-bot
skill to new bots. load_skill is a reserved tool name, so a custom tool
cannot shadow it, and a built-in wins a clash.

Managed on Settings > Skills; ExtensionRow links each kind to its own page
and a skill never reports a failure, since nothing connects or runs.
This commit is contained in:
2026-10-22 12:31:42 +01:00
parent 379ef35e08
commit 08d02e6033
62 changed files with 1438 additions and 79 deletions
@@ -1,5 +1,6 @@
import grabBotExtensions from "@/src/functions/backend/db/extensions/grab-bot-extensions";
import grabRunEnvs from "@/src/functions/backend/db/extensions/grab-run-envs";
import type { SkillRecord } from "@/src/types";
import mcpToolName from "@/src/utils/mcp-tool-name";
import redactSecrets from "@/src/utils/redact-secrets";
import mcpHarnessTool from "../../mcp/mcp-harness-tool";
@@ -30,6 +31,11 @@ export type OpenBotExtensions = {
* with no tools listed), so the bot can say so. Empty otherwise.
*/
note: string;
/**
* The skills the run has, for `openBotSkills`: read from the same query
* as the MCP servers and tools.
*/
skills: SkillRecord[];
close: () => Promise<void>;
};
@@ -47,9 +53,11 @@ export default async function openBotExtensions({
taken = [],
signal,
}: Params): Promise<OpenBotExtensions> {
const { servers, tools } = await grabBotExtensions({ bot_id, workspace_id });
const { servers, tools, skills } = await grabBotExtensions({ bot_id, workspace_id });
if (!servers.length && !tools.length) return { tools: [], note: "", close: async () => {} };
if (!servers.length && !tools.length) {
return { tools: [], note: "", skills, close: async () => {} };
}
const envs = await grabRunEnvs({ bot_id, workspace_id });
const secrets = envs.filter((env) => env.secret);
@@ -98,6 +106,7 @@ export default async function openBotExtensions({
return {
tools: result,
skills,
note: notes.length
? `These MCP servers were set up for you but you can't use them right now; if the user needs them, say so (an admin fixes them in Settings, under MCPs):\n${notes.join("\n")}`
: "",