fix(cli): allowed extension tools in allowlists

Deferred --tools validation until extension discovery, then validated against built-in and registered tool names while preserving strict rejection of unknown names.

Fixes #8421
This commit is contained in:
roboomp
2026-08-13 08:53:59 +00:00
parent 326d24bd40
commit 6980275a50
9 changed files with 74 additions and 30 deletions
@@ -113,6 +113,7 @@ describe("parseArgs — unrecognized flag tracking (#2459)", () => {
it("propagates unrecognizedFlags through applyExtensionFlags so callers can surface them", () => {
const runner = {
getFlags: () => new Map<string, { type: "boolean" | "string" }>([["spawn-peer", { type: "string" }]]),
getToolNames: () => [],
setFlagValue: () => {},
};
const parsed = applyExtensionFlags(runner, ["--spawn-peer", "reviewer", "--typo"]);
@@ -15,6 +15,10 @@ class FakeExtensionFlagSink implements ExtensionFlagSink {
]);
}
getToolNames(): readonly string[] {
return [];
}
setFlagValue(name: string, value: boolean | string): void {
this.#values.set(name, value);
}
@@ -103,6 +103,7 @@ describe("extension flags vs initial message", () => {
const sessionId = "019ea530-ffff-7000-8000-000000000000";
const sink: ExtensionFlagSink = {
getFlags: () => extFlags,
getToolNames: () => [],
setFlagValue: () => {},
};
const parsed = applyExtensionFlags(sink, ["--continue", sessionId]);
@@ -123,6 +124,7 @@ describe("extension flags vs initial message", () => {
const rawArgs = ["--continue", sessionId, "--spawn-peer", "reviewer", "do next"];
const sink: ExtensionFlagSink = {
getFlags: () => extFlags,
getToolNames: () => [],
setFlagValue: () => {},
};
const parsed = applyExtensionFlags(sink, rawArgs);
@@ -147,6 +149,7 @@ describe("extension flags vs initial message", () => {
const sink: ExtensionFlagSink = {
getFlags: () => extFlags,
getToolNames: () => [],
setFlagValue: () => {},
};
const extensionArgs = applyExtensionFlags(sink, rawArgs);
@@ -212,6 +215,7 @@ describe("applyExtensionFlags (single-parser flag resolution)", () => {
const values = new Map<string, boolean | string>();
return {
values,
getToolNames: () => [],
getFlags: () => flagMap,
setFlagValue: (name, value) => {
values.set(name, value);
@@ -221,8 +225,8 @@ describe("applyExtensionFlags (single-parser flag resolution)", () => {
it("returns null when there is no runner", () => {
expect(applyExtensionFlags(undefined, ["--spawn-peer", "x", "task"])).toBeNull();
});
it("returns null when the runner registered no flags", () => {
expect(applyExtensionFlags(fakeRunner({}), ["--whatever", "task"])).toBeNull();
it("reparses with an empty extension registry so unknown flags remain visible", () => {
expect(applyExtensionFlags(fakeRunner({}), ["--whatever", "task"])?.unrecognizedFlags).toEqual(["--whatever"]);
});
it("applies and strips a string flag in space form", () => {
const runner = fakeRunner({ "spawn-peer": "string" });
@@ -330,6 +334,7 @@ describe("registerFlag with built-in-named flags (r3323473227)", () => {
);
const sink: ExtensionFlagSink = {
getFlags: () => ExtensionRunner.aggregateFlags([ext]),
getToolNames: () => [],
setFlagValue: (name, value) => {
runtime.flagValues.set(name, value);
},
+12 -7
View File
@@ -85,19 +85,24 @@ describe("--session-dir", () => {
});
});
describe("--tools legacy aliases", () => {
describe("--tools validation", () => {
it("maps search and find to grep and glob", () => {
const result = parseArgs(["--tools", "search,find,grep"]);
expect(result.tools).toEqual(["grep", "glob"]);
});
it("rejects unknown tool names instead of silently narrowing the toolset", () => {
// Removed tools (ssh, job, irc, launch, search_tool_bm25) used to be
// dropped with only a log-file warning, so `--tools bash,ssh` ran with
// just bash and no visible notice.
expect(() => parseArgs(["--tools", "bash,ssh"])).toThrow(CliUsageError);
expect(() => parseArgs(["--tools", "bash,ssh"])).toThrow(/Unknown tool in --tools: ssh/);
it("defers unknown-name validation until extension discovery", () => {
expect(parseArgs(["--tools", "bash,intercom"]).tools).toEqual(["bash", "intercom"]);
});
it("accepts registered extension tools and still rejects unknown names after discovery", () => {
const extensionFlags = new Map<string, { type: "boolean" | "string" }>();
expect(parseArgs(["--tools", "read,intercom"], extensionFlags, ["intercom"]).tools).toEqual(["read", "intercom"]);
expect(() => parseArgs(["--tools", "bash,ssh"], extensionFlags, ["intercom"])).toThrow(CliUsageError);
expect(() => parseArgs(["--tools", "bash,ssh"], extensionFlags, ["intercom"])).toThrow(
/Unknown tool in --tools: ssh/,
);
});
});