feat(coding-agent): add exclude tools option closes #5109

This commit is contained in:
Mario Zechner
2026-05-28 23:49:01 +02:00
parent 1ab2899800
commit 9380d5f2e4
12 changed files with 147 additions and 6 deletions

View File

@@ -308,6 +308,16 @@ describe("parseArgs", () => {
expect(result.tools).toEqual(["read", "bash"]);
});
test("parses --exclude-tools flag", () => {
const result = parseArgs(["--exclude-tools", "read,bash"]);
expect(result.excludeTools).toEqual(["read", "bash"]);
});
test("parses -xt shorthand", () => {
const result = parseArgs(["-xt", "read,bash"]);
expect(result.excludeTools).toEqual(["read", "bash"]);
});
test("parses --no-tools with explicit --tools flags", () => {
const result = parseArgs(["--no-tools", "--tools", "read,bash"]);
expect(result.noTools).toBe(true);

View File

@@ -60,6 +60,9 @@ export interface HarnessOptions {
settings?: Partial<Settings>;
systemPrompt?: string;
tools?: AgentTool[];
initialActiveToolNames?: string[];
allowedToolNames?: string[];
excludedToolNames?: string[];
resourceLoader?: ResourceLoader;
extensionFactories?: Array<ExtensionFactory | CreateTestExtensionsResultInput>;
withConfiguredAuth?: boolean;
@@ -173,6 +176,9 @@ export async function createHarness(options: HarnessOptions = {}): Promise<Harne
modelRegistry,
resourceLoader,
baseToolsOverride: toolMap,
initialActiveToolNames: options.initialActiveToolNames,
allowedToolNames: options.allowedToolNames,
excludedToolNames: options.excludedToolNames,
extensionRunnerRef,
});

View File

@@ -0,0 +1,81 @@
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import type { ExtensionFactory } from "../../../src/index.ts";
import { createHarness } from "../harness.ts";
function toolNames(tools: Array<{ name: string }>): string[] {
return tools.map((tool) => tool.name).sort();
}
describe("regression #5109: exclude tools", () => {
const extensionFactories: ExtensionFactory[] = [
(pi) => {
pi.on("session_start", () => {
pi.registerTool({
name: "ask_question",
label: "Ask Question",
description: "Ask a question",
promptSnippet: "Ask a question",
parameters: Type.Object({}),
execute: async () => ({
content: [{ type: "text", text: "ok" }],
details: {},
}),
});
pi.registerTool({
name: "dynamic_tool",
label: "Dynamic Tool",
description: "Dynamic test tool",
promptSnippet: "Run dynamic test behavior",
parameters: Type.Object({}),
execute: async () => ({
content: [{ type: "text", text: "ok" }],
details: {},
}),
});
});
},
];
it("filters built-in and extension tools from available and active tools", async () => {
const harness = await createHarness({
excludedToolNames: ["read", "ask_question"],
extensionFactories,
});
try {
await harness.session.bindExtensions({});
const allToolNames = toolNames(harness.session.getAllTools());
expect(allToolNames).not.toContain("read");
expect(allToolNames).not.toContain("ask_question");
expect(allToolNames).toContain("bash");
expect(allToolNames).toContain("dynamic_tool");
expect(harness.session.getActiveToolNames().sort()).toEqual(["bash", "dynamic_tool", "edit", "write"]);
expect(harness.session.systemPrompt).not.toContain("- read:");
expect(harness.session.systemPrompt).not.toContain("ask_question");
expect(harness.session.systemPrompt).toContain("- dynamic_tool: Run dynamic test behavior");
} finally {
harness.cleanup();
}
});
it("lets excluded tools override the allowlist", async () => {
const harness = await createHarness({
allowedToolNames: ["read", "bash", "ask_question"],
excludedToolNames: ["read", "ask_question"],
initialActiveToolNames: ["read", "bash", "ask_question"],
extensionFactories,
});
try {
await harness.session.bindExtensions({});
expect(toolNames(harness.session.getAllTools())).toEqual(["bash"]);
expect(harness.session.getActiveToolNames()).toEqual(["bash"]);
expect(harness.session.systemPrompt).toContain("- bash:");
expect(harness.session.systemPrompt).not.toContain("- read:");
expect(harness.session.systemPrompt).not.toContain("ask_question");
} finally {
harness.cleanup();
}
});
});