diff --git a/assistant/src/__tests__/plugin-bootstrap.test.ts b/assistant/src/__tests__/plugin-bootstrap.test.ts index 7b995066e26..eb0359158bd 100644 --- a/assistant/src/__tests__/plugin-bootstrap.test.ts +++ b/assistant/src/__tests__/plugin-bootstrap.test.ts @@ -372,6 +372,7 @@ describe("plugin bootstrap", () => { { name: "gated-off-tool", description: "should not be registered", + category: "test", defaultRiskLevel: RiskLevel.Low, executionTarget: "sandbox", input_schema: { type: "object", properties: {}, required: [] }, diff --git a/assistant/src/__tests__/plugin-tool-contribution.test.ts b/assistant/src/__tests__/plugin-tool-contribution.test.ts index c38c9e2b66c..173e9a5f434 100644 --- a/assistant/src/__tests__/plugin-tool-contribution.test.ts +++ b/assistant/src/__tests__/plugin-tool-contribution.test.ts @@ -62,7 +62,7 @@ import { unregisterPluginTools, } from "../tools/registry.js"; import type { - LoadedTool, + Tool, ToolContext, ToolExecutionResult, } from "../tools/types.js"; @@ -84,14 +84,15 @@ const fakeCtx: DaemonContext = { function makeFakeTool( name: string, - extras: Partial = {}, -): LoadedTool { + extras: Partial = {}, +): Tool { return { name, description: `Fake ${name}`, defaultRiskLevel: RiskLevel.Low, executionTarget: "sandbox", input_schema: { type: "object", properties: {}, required: [] }, + category: "", async execute( _input: Record, _context: ToolContext, @@ -336,7 +337,7 @@ describe("registerPluginTools / unregisterPluginTools helpers", () => { ...makeFakeTool("pt_spoof"), origin: "skill", owner: { kind: "skill", id: "some-other-skill" }, - } as unknown as LoadedTool; + } as unknown as Tool; registerPluginTools("my-plugin", [spoofed]); expect(getTool("pt_spoof")).toBeDefined(); expect(getToolOwner("pt_spoof")).toEqual({ diff --git a/assistant/src/__tests__/plugin-types.test.ts b/assistant/src/__tests__/plugin-types.test.ts index 4402f7ac18b..85a5cf38b12 100644 --- a/assistant/src/__tests__/plugin-types.test.ts +++ b/assistant/src/__tests__/plugin-types.test.ts @@ -46,7 +46,7 @@ import { type ToolResultTruncateResult, type TurnContext, } from "../plugins/types.js"; -import type { LoadedTool } from "../tools/types.js"; +import type { Tool } from "../tools/types.js"; const sampleTrust: TrustContext = { sourceChannel: "vellum", @@ -207,12 +207,13 @@ describe("plugin core types", () => { }, }; - const sampleTool: LoadedTool = { + const sampleTool: Tool = { name: "sample-tool", description: "Sample plugin tool", defaultRiskLevel: RiskLevel.Low, executionTarget: "sandbox", input_schema: { type: "object", properties: {}, required: [] }, + category: "", async execute() { return { content: "ok", isError: false }; }, diff --git a/assistant/src/plugins/external-plugin-loader.ts b/assistant/src/plugins/external-plugin-loader.ts index a5267952e38..7c8d3734c99 100644 --- a/assistant/src/plugins/external-plugin-loader.ts +++ b/assistant/src/plugins/external-plugin-loader.ts @@ -50,7 +50,7 @@ import { z } from "zod"; import assistantPkg from "../../package.json" with { type: "json" }; import { finalizeTool } from "../tools/tool-defaults.js"; -import type { LoadedTool, ToolDefinition } from "../tools/types.js"; +import type { Tool, ToolDefinition } from "../tools/types.js"; import { getLogger } from "../util/logger.js"; import { registerPlugin } from "./registry.js"; import type { @@ -276,7 +276,7 @@ async function buildPluginFromDir(pluginDir: string): Promise { const hooks = await loadHooks(pluginDir, name); if (hooks !== undefined) plugin.hooks = hooks; - const tools: LoadedTool[] = []; + const tools: Tool[] = []; for (const { name: toolName, path: toolPath } of listSurfaceDir( join(pluginDir, "tools"), )) { diff --git a/assistant/src/plugins/types.ts b/assistant/src/plugins/types.ts index 362806aadb8..d3e142372a0 100644 --- a/assistant/src/plugins/types.ts +++ b/assistant/src/plugins/types.ts @@ -44,7 +44,7 @@ import type { } from "../providers/types.js"; import type { SkillRoute } from "../runtime/skill-route-registry.js"; import type { - LoadedTool, + Tool, ToolContext, ToolExecutionResult, } from "../tools/types.js"; @@ -1157,10 +1157,10 @@ export interface Plugin { * `@vellumai/plugin-api`); the loader derives `name` from the * `tools/.ts` basename and runs the definition through * `finalizeTool` to fill omitted required fields, producing the - * `LoadedTool` values stored here. Category / ownership metadata is + * `Tool` values stored here. Category / ownership metadata is * stamped by `registerPluginTools` at registration time. */ - tools?: LoadedTool[]; + tools?: Tool[]; /** HTTP route registrations served by the assistant. */ routes?: PluginRouteRegistration[]; /** Skill registrations loaded at startup. */ diff --git a/assistant/src/tools/execution-target.ts b/assistant/src/tools/execution-target.ts index f5dfb232e37..c857f1474bd 100644 --- a/assistant/src/tools/execution-target.ts +++ b/assistant/src/tools/execution-target.ts @@ -15,7 +15,7 @@ export interface ManifestOverride { * 3. Default sandbox. * * Called once per tool at load/construction time. The returned value is - * stamped onto every `LoadedTool`, so runtime reads are just a field read. + * stamped onto every `Tool`, so runtime reads are just a field read. */ export function resolveExecutionTarget(tool: { name: string; diff --git a/assistant/src/tools/registry.ts b/assistant/src/tools/registry.ts index 6e55a6f25e0..7889a817ee5 100644 --- a/assistant/src/tools/registry.ts +++ b/assistant/src/tools/registry.ts @@ -9,7 +9,7 @@ import { hostFileWriteTool } from "./host-filesystem/write.js"; import { hostShellTool } from "./host-terminal/host-shell.js"; import { toProviderSafeToolName } from "./provider-tool-name.js"; import { registerSystemTools } from "./system/register.js"; -import type { LoadedTool, OwnerInfo, Tool } from "./types.js"; +import type { OwnerInfo, Tool } from "./types.js"; import { allUiSurfaceTools } from "./ui-surface/definitions.js"; import { registerUiSurfaceTools } from "./ui-surface/registry.js"; @@ -235,7 +235,7 @@ export function registerSkillTools(skillId: string, newTools: Tool[]): Tool[] { */ export function registerPluginTools( pluginName: string, - newTools: LoadedTool[], + newTools: Tool[], ): Tool[] { const stamped: Tool[] = newTools.map((pluginTool) => { const tool: Tool = { diff --git a/assistant/src/tools/tool-defaults.ts b/assistant/src/tools/tool-defaults.ts index ecdc464a13a..01b04269336 100644 --- a/assistant/src/tools/tool-defaults.ts +++ b/assistant/src/tools/tool-defaults.ts @@ -1,18 +1,18 @@ /** * Single source of truth for the defaults applied when a `ToolDefinition` * omits one of the normally-required fields, plus the `finalizeTool` - * helper that lifts a `ToolDefinition` into a `LoadedTool`. + * helper that lifts a `ToolDefinition` into a `Tool`. * * Plugins, external loaders, and any other registration boundary that * accepts loose `ToolDefinition` objects from authors must run them * through `finalizeTool` before handing the result to a `registerXxxTools` * call. The registry types make this a hard rule: every registered tool - * is a `LoadedTool` (`Required & { name }`). + * is a `Tool` (`Required & { name }`). */ import type { - LoadedTool, RiskLevel, + Tool, ToolDefinition, ToolExecutionResult, } from "./types.js"; @@ -31,6 +31,10 @@ import type { * - `executionTarget` defaults to `sandbox` — author-supplied tool code * runs in the assistant container by default; opt in to `host` when * the tool proxies work to the connected client. + * - `category` defaults to empty — Slack channel `allowedToolCategories` + * policy denies uncategorized tools when a category allow-list is set, + * which is the correct deny-by-default for tools the author didn't + * explicitly bucket. * * `execute` has no constant default because the default closure needs to * close over the tool's name to produce a useful error message; see @@ -45,12 +49,13 @@ export const TOOL_DEFAULTS = Object.freeze({ additionalProperties: false, }) as object, executionTarget: "sandbox" as const, + category: "", }); /** - * Fill the four normally-required `ToolDefinition` fields with documented + * Fill the five normally-required `ToolDefinition` fields with documented * defaults when the author omitted them, attach the registration-time - * `name`, and return a `LoadedTool` that is safe to hand to a + * `name`, and return a `Tool` that is safe to hand to a * `registerXxxTools` call. * * The default `execute` returns an error result so the model sees a clear @@ -61,7 +66,7 @@ export const TOOL_DEFAULTS = Object.freeze({ export function finalizeTool( tool: ToolDefinition, name: string, -): LoadedTool { +): Tool { const description = typeof tool.description === "string" ? tool.description @@ -82,6 +87,7 @@ export function finalizeTool( isError: true, }); const executionTarget = tool.executionTarget ?? TOOL_DEFAULTS.executionTarget; + const category = tool.category ?? TOOL_DEFAULTS.category; return { ...tool, name, @@ -90,5 +96,6 @@ export function finalizeTool( input_schema, executionTarget, execute, + category, }; } diff --git a/assistant/src/tools/types.ts b/assistant/src/tools/types.ts index c6161a49d15..75c0db7b4e2 100644 --- a/assistant/src/tools/types.ts +++ b/assistant/src/tools/types.ts @@ -335,10 +335,12 @@ export interface ToolDefinition { input: Record, context: ToolContext, ) => Promise; + /** Tool category used for Slack channel `allowedToolCategories` enforcement. */ + category?: string; } /** Tool after the loader has derived its name and filled defaults. */ -export type LoadedTool = Required & { +export type Tool = Required & { name: string; }; @@ -355,7 +357,3 @@ export interface OwnerInfo { /** ID of the owning extension (skill id / plugin name / MCP server id). */ id: string; } - -export interface Tool extends LoadedTool { - category: string; -}