From 6980275a50fef68a89f4715b8bc5664aa2df4ded Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 13 Aug 2026 08:53:59 +0000 Subject: [PATCH 1/2] 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 --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/src/cli/args.ts | 14 +++++++++++--- .../coding-agent/src/cli/extension-flags.ts | 17 +++++++---------- packages/coding-agent/src/cli/flag-tables.ts | 18 +++++++++++------- packages/coding-agent/src/main.ts | 18 +++++++++++++++++- .../test/cli-unknown-flag.test.ts | 1 + .../test/extension-flag-dispatch.test.ts | 4 ++++ .../extension-flag-initial-message.test.ts | 9 +++++++-- .../coding-agent/test/flag-tables.test.ts | 19 ++++++++++++------- 9 files changed, 74 insertions(+), 30 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 14334fddd..a52f46fc1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed extension-registered tool names being rejected by `--tools` before extension discovery, preventing least-privilege sessions from allowlisting plugin tools ([#8421](https://github.com/can1357/oh-my-pi/issues/8421)). + ## [17.3.0] - 2026-08-13 ### Breaking Changes diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index a2785eb96..67784ffd3 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -108,7 +108,8 @@ export interface Args { const PARSE_DEPS: ParseDeps = { logger, parseThinking: parseCliThinkingLevel, - builtinToolNames: [...BUILTIN_TOOL_NAMES, ...HIDDEN_TOOL_NAMES], + toolNames: [...BUILTIN_TOOL_NAMES, ...HIDDEN_TOOL_NAMES], + validateToolNames: false, normalizeToolNames, thinkingEfforts: CLI_THINKING_LEVELS, }; @@ -142,12 +143,19 @@ function consumeBuiltInStringValue(flag: string, args: string[], valueIndex: num return { value, index: valueIndex }; } -export function parseArgs(inputArgs: string[], extensionFlags?: Map): Args { +export function parseArgs( + inputArgs: string[], + extensionFlags?: Map, + extensionToolNames: readonly string[] = [], +): Args { // Work on a copy: the `--option=value` handling below splices the value // into the array, and callers reuse the same argv (the post-extension // reparse in `runRootCommand` parses it a second time). Mutating the input // would corrupt that later parse, so never touch the caller's array. const args = [...inputArgs]; + const parseDeps: ParseDeps = extensionFlags + ? { ...PARSE_DEPS, toolNames: [...PARSE_DEPS.toolNames, ...extensionToolNames], validateToolNames: true } + : PARSE_DEPS; const result: Args = { messages: [], fileArgs: [], @@ -214,7 +222,7 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map; + getToolNames(): readonly string[]; setFlagValue(name: string, value: boolean | string): void; } @@ -29,18 +30,14 @@ export interface ExtensionFlagSink { * semantics and surfaces in `unknownFlags` — without consuming the following * message or overwriting the built-in field. No built-in name list to maintain. * - * Returns `null` when there is no sink or no registered extension flags, in - * which case the caller keeps its original startup parse (an extension-aware - * re-parse would be identical anyway). + * Returns `null` only when there is no sink. Once extensions have loaded, the + * reparse always runs so `--tools` can be validated against their registered + * tools even when no extension registered CLI flags. */ export function applyExtensionFlags(runner: ExtensionFlagSink | undefined, rawArgs: string[]): Args | null { - const extensionFlags = runner?.getFlags(); - if (!runner || !extensionFlags || extensionFlags.size === 0) { - return null; - } - const parsed = parseArgs(rawArgs, extensionFlags); - // `parseArgs` only records registered extension flags in `unknownFlags`, so - // every entry here is a flag this runner owns that was actually passed. + if (!runner) return null; + const parsed = parseArgs(rawArgs, runner.getFlags(), runner.getToolNames()); + // `parseArgs` records extension flag values in `unknownFlags`. for (const [name, value] of parsed.unknownFlags) { runner.setFlagValue(name, value); } diff --git a/packages/coding-agent/src/cli/flag-tables.ts b/packages/coding-agent/src/cli/flag-tables.ts index 6c50a177b..2d8eae8cb 100644 --- a/packages/coding-agent/src/cli/flag-tables.ts +++ b/packages/coding-agent/src/cli/flag-tables.ts @@ -47,7 +47,8 @@ import { CliUsageError } from "./usage-error"; export interface ParseDeps { logger: { warn: (message: string, meta?: Record) => void }; parseThinking: (value: string | null | undefined) => ConfiguredThinkingLevel | undefined; - builtinToolNames: readonly string[]; + toolNames: readonly string[]; + validateToolNames: boolean; normalizeToolNames: (values: Iterable) => string[]; thinkingEfforts: readonly string[]; } @@ -191,12 +192,15 @@ export const STRING_SETTERS: Record = { ); // An unknown name silently narrowing the toolset is worse than a failed // launch: scripts keep running believing the tool is available (e.g. a - // stale `--tools bash,ssh` after the ssh tool's removal). - const unknown = names.filter(name => !deps.builtinToolNames.includes(name)); - if (unknown.length > 0) { - throw new CliUsageError( - `Unknown tool${unknown.length === 1 ? "" : "s"} in --tools: ${unknown.join(", ")}. Valid tools: ${deps.builtinToolNames.join(", ")}.`, - ); + // stale `--tools bash,ssh` after the ssh tool's removal). The startup + // parse defers this check until extensions have registered their tools. + if (deps.validateToolNames) { + const unknown = names.filter(name => !deps.toolNames.includes(name)); + if (unknown.length > 0) { + throw new CliUsageError( + `Unknown tool${unknown.length === 1 ? "" : "s"} in --tools: ${unknown.join(", ")}. Valid tools: ${deps.toolNames.join(", ")}.`, + ); + } } result.tools = names; }, diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index d1e98a749..bea616dd6 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -425,7 +425,19 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess if (args.parsedArgs.apiKey && !args.baseOptions.model && nextSession.model) { args.authStorage.setRuntimeApiKey(nextSession.model.provider, args.parsedArgs.apiKey); } - applyExtensionFlags(nextSession.extensionRunner, args.rawArgs); + const runner = nextSession.extensionRunner; + applyExtensionFlags( + runner + ? { + getFlags: () => runner.getFlags(), + getToolNames: () => runner.getAllRegisteredTools().map(tool => tool.definition.name), + setFlagValue: (name, value) => { + runner.setFlagValue(name, value); + }, + } + : undefined, + args.rawArgs, + ); return nextSession; }; } @@ -1615,6 +1627,10 @@ export async function runRootCommand( : await loadSessionExtensions(sessionOptions, cwd, settingsInstance, eventBus); const extensionFlagSink: ExtensionFlagSink = { getFlags: () => ExtensionRunner.aggregateFlags(extensionsResult.extensions), + getToolNames: () => + extensionsResult.extensions.flatMap(extension => + Array.from(extension.tools.values(), tool => tool.definition.name), + ), setFlagValue: (name, value) => { extensionsResult.runtime.flagValues.set(name, value); }, diff --git a/packages/coding-agent/test/cli-unknown-flag.test.ts b/packages/coding-agent/test/cli-unknown-flag.test.ts index ba142dcf8..0d5f93738 100644 --- a/packages/coding-agent/test/cli-unknown-flag.test.ts +++ b/packages/coding-agent/test/cli-unknown-flag.test.ts @@ -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([["spawn-peer", { type: "string" }]]), + getToolNames: () => [], setFlagValue: () => {}, }; const parsed = applyExtensionFlags(runner, ["--spawn-peer", "reviewer", "--typo"]); diff --git a/packages/coding-agent/test/extension-flag-dispatch.test.ts b/packages/coding-agent/test/extension-flag-dispatch.test.ts index b61706ea4..c1c267de2 100644 --- a/packages/coding-agent/test/extension-flag-dispatch.test.ts +++ b/packages/coding-agent/test/extension-flag-dispatch.test.ts @@ -15,6 +15,10 @@ class FakeExtensionFlagSink implements ExtensionFlagSink { ]); } + getToolNames(): readonly string[] { + return []; + } + setFlagValue(name: string, value: boolean | string): void { this.#values.set(name, value); } diff --git a/packages/coding-agent/test/extension-flag-initial-message.test.ts b/packages/coding-agent/test/extension-flag-initial-message.test.ts index 0c53aa5a7..252274716 100644 --- a/packages/coding-agent/test/extension-flag-initial-message.test.ts +++ b/packages/coding-agent/test/extension-flag-initial-message.test.ts @@ -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(); 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); }, diff --git a/packages/coding-agent/test/flag-tables.test.ts b/packages/coding-agent/test/flag-tables.test.ts index 62173cc08..e24a071ba 100644 --- a/packages/coding-agent/test/flag-tables.test.ts +++ b/packages/coding-agent/test/flag-tables.test.ts @@ -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(); + 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/, + ); }); }); From 3749478239d103b3d1faa6ff349e4d8580bf73c1 Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 13 Aug 2026 09:12:12 +0000 Subject: [PATCH 2/2] fix(cli): validated allowlists after tool discovery Moved strict --tools validation to the completed session registry so extension modules, custom tool directories, and plugin manifest tools are all eligible while unknown names still fail startup. --- packages/coding-agent/src/cli/args.ts | 25 +++++++++++-------- .../coding-agent/src/cli/extension-flags.ts | 3 +-- packages/coding-agent/src/cli/flag-tables.ts | 16 ++---------- packages/coding-agent/src/main.ts | 14 ++++++----- .../test/cli-unknown-flag.test.ts | 1 - .../extension-flag-initial-message.test.ts | 5 ---- .../coding-agent/test/flag-tables.test.ts | 22 ++++++++++------ 7 files changed, 39 insertions(+), 47 deletions(-) diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 67784ffd3..412e78289 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -6,7 +6,7 @@ import { $env, APP_NAME, logger } from "@oh-my-pi/pi-utils"; import chalk from "@oh-my-pi/pi-utils/chalk"; import type { ServiceTierOpenAISettingValue } from "../config/service-tier"; import { CLI_THINKING_LEVELS, type ConfiguredThinkingLevel, parseCliThinkingLevel } from "../thinking"; -import { BUILTIN_TOOL_NAMES, HIDDEN_TOOL_NAMES, normalizeToolNames } from "../tools/builtin-names"; +import { normalizeToolNames } from "../tools/builtin-names"; import { OPTIONAL_FLAGS, OPTIONAL_VALUE_FLAGS, @@ -108,8 +108,6 @@ export interface Args { const PARSE_DEPS: ParseDeps = { logger, parseThinking: parseCliThinkingLevel, - toolNames: [...BUILTIN_TOOL_NAMES, ...HIDDEN_TOOL_NAMES], - validateToolNames: false, normalizeToolNames, thinkingEfforts: CLI_THINKING_LEVELS, }; @@ -143,19 +141,13 @@ function consumeBuiltInStringValue(flag: string, args: string[], valueIndex: num return { value, index: valueIndex }; } -export function parseArgs( - inputArgs: string[], - extensionFlags?: Map, - extensionToolNames: readonly string[] = [], -): Args { +export function parseArgs(inputArgs: string[], extensionFlags?: Map): Args { // Work on a copy: the `--option=value` handling below splices the value // into the array, and callers reuse the same argv (the post-extension // reparse in `runRootCommand` parses it a second time). Mutating the input // would corrupt that later parse, so never touch the caller's array. const args = [...inputArgs]; - const parseDeps: ParseDeps = extensionFlags - ? { ...PARSE_DEPS, toolNames: [...PARSE_DEPS.toolNames, ...extensionToolNames], validateToolNames: true } - : PARSE_DEPS; + const parseDeps = PARSE_DEPS; const result: Args = { messages: [], fileArgs: [], @@ -340,6 +332,17 @@ export function parseArgs( return result; } +/** Reject requested tool names absent from the fully discovered session registry. */ +export function validateToolNames(requested: readonly string[] | undefined, known: readonly string[]): void { + if (!requested) return; + const knownNames = new Set(known); + const unknown = requested.filter(name => !knownNames.has(name)); + if (unknown.length === 0) return; + throw new CliUsageError( + `Unknown tool${unknown.length === 1 ? "" : "s"} in --tools: ${unknown.join(", ")}. Valid tools: ${known.join(", ")}.`, + ); +} + /** * Emit a stderr error listing the unrecognized flags and return `true` when * there were any. Caller is expected to exit with a non-zero status. Splitting diff --git a/packages/coding-agent/src/cli/extension-flags.ts b/packages/coding-agent/src/cli/extension-flags.ts index 3e106d1f7..997b0db97 100644 --- a/packages/coding-agent/src/cli/extension-flags.ts +++ b/packages/coding-agent/src/cli/extension-flags.ts @@ -8,7 +8,6 @@ import { type Args, parseArgs } from "./args"; */ export interface ExtensionFlagSink { getFlags(): Map; - getToolNames(): readonly string[]; setFlagValue(name: string, value: boolean | string): void; } @@ -36,7 +35,7 @@ export interface ExtensionFlagSink { */ export function applyExtensionFlags(runner: ExtensionFlagSink | undefined, rawArgs: string[]): Args | null { if (!runner) return null; - const parsed = parseArgs(rawArgs, runner.getFlags(), runner.getToolNames()); + const parsed = parseArgs(rawArgs, runner.getFlags()); // `parseArgs` records extension flag values in `unknownFlags`. for (const [name, value] of parsed.unknownFlags) { runner.setFlagValue(name, value); diff --git a/packages/coding-agent/src/cli/flag-tables.ts b/packages/coding-agent/src/cli/flag-tables.ts index 2d8eae8cb..7e56921ba 100644 --- a/packages/coding-agent/src/cli/flag-tables.ts +++ b/packages/coding-agent/src/cli/flag-tables.ts @@ -47,8 +47,6 @@ import { CliUsageError } from "./usage-error"; export interface ParseDeps { logger: { warn: (message: string, meta?: Record) => void }; parseThinking: (value: string | null | undefined) => ConfiguredThinkingLevel | undefined; - toolNames: readonly string[]; - validateToolNames: boolean; normalizeToolNames: (values: Iterable) => string[]; thinkingEfforts: readonly string[]; } @@ -190,18 +188,8 @@ export const STRING_SETTERS: Record = { .map(s => s.trim()) .filter(Boolean), ); - // An unknown name silently narrowing the toolset is worse than a failed - // launch: scripts keep running believing the tool is available (e.g. a - // stale `--tools bash,ssh` after the ssh tool's removal). The startup - // parse defers this check until extensions have registered their tools. - if (deps.validateToolNames) { - const unknown = names.filter(name => !deps.toolNames.includes(name)); - if (unknown.length > 0) { - throw new CliUsageError( - `Unknown tool${unknown.length === 1 ? "" : "s"} in --tools: ${unknown.join(", ")}. Valid tools: ${deps.toolNames.join(", ")}.`, - ); - } - } + // Validation runs after session tool discovery. At this point extension, + // custom, plugin-manifest, and MCP tools are not all known yet. result.tools = names; }, "--thinking": (result, value, deps) => { diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index bea616dd6..54ff3b1d1 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -23,7 +23,7 @@ import { } from "@oh-my-pi/pi-utils"; import chalk from "@oh-my-pi/pi-utils/chalk"; import { reset as resetCapabilities } from "./capability"; -import { type Args, reportUnrecognizedFlags } from "./cli/args"; +import { type Args, reportUnrecognizedFlags, validateToolNames } from "./cli/args"; import { applyExtensionFlags, type ExtensionFlagSink } from "./cli/extension-flags"; import { processFileArguments } from "./cli/file-processor"; import { buildInitialMessage } from "./cli/initial-message"; @@ -430,7 +430,6 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess runner ? { getFlags: () => runner.getFlags(), - getToolNames: () => runner.getAllRegisteredTools().map(tool => tool.definition.name), setFlagValue: (name, value) => { runner.setFlagValue(name, value); }, @@ -1627,10 +1626,6 @@ export async function runRootCommand( : await loadSessionExtensions(sessionOptions, cwd, settingsInstance, eventBus); const extensionFlagSink: ExtensionFlagSink = { getFlags: () => ExtensionRunner.aggregateFlags(extensionsResult.extensions), - getToolNames: () => - extensionsResult.extensions.flatMap(extension => - Array.from(extension.tools.values(), tool => tool.definition.name), - ), setFlagValue: (name, value) => { extensionsResult.runtime.flagValues.set(name, value); }, @@ -1701,6 +1696,13 @@ export async function runRootCommand( preloadedExtensions: extensionsResult, }); + try { + validateToolNames(initialArgs.tools, session.getAllToolNames()); + } catch (error) { + await session.dispose(); + throw error; + } + // Cold-revive support: a `parked` subagent ref restored from disk (Agent Hub // scan, collab mirror, resumed process) has a sessionFile but no in-memory // reviver, so `ensureLive` (IRC sends, hub focus) would refuse it. Install a diff --git a/packages/coding-agent/test/cli-unknown-flag.test.ts b/packages/coding-agent/test/cli-unknown-flag.test.ts index 0d5f93738..ba142dcf8 100644 --- a/packages/coding-agent/test/cli-unknown-flag.test.ts +++ b/packages/coding-agent/test/cli-unknown-flag.test.ts @@ -113,7 +113,6 @@ describe("parseArgs — unrecognized flag tracking (#2459)", () => { it("propagates unrecognizedFlags through applyExtensionFlags so callers can surface them", () => { const runner = { getFlags: () => new Map([["spawn-peer", { type: "string" }]]), - getToolNames: () => [], setFlagValue: () => {}, }; const parsed = applyExtensionFlags(runner, ["--spawn-peer", "reviewer", "--typo"]); diff --git a/packages/coding-agent/test/extension-flag-initial-message.test.ts b/packages/coding-agent/test/extension-flag-initial-message.test.ts index 252274716..c24113318 100644 --- a/packages/coding-agent/test/extension-flag-initial-message.test.ts +++ b/packages/coding-agent/test/extension-flag-initial-message.test.ts @@ -103,7 +103,6 @@ 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]); @@ -124,7 +123,6 @@ 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); @@ -149,7 +147,6 @@ describe("extension flags vs initial message", () => { const sink: ExtensionFlagSink = { getFlags: () => extFlags, - getToolNames: () => [], setFlagValue: () => {}, }; const extensionArgs = applyExtensionFlags(sink, rawArgs); @@ -215,7 +212,6 @@ describe("applyExtensionFlags (single-parser flag resolution)", () => { const values = new Map(); return { values, - getToolNames: () => [], getFlags: () => flagMap, setFlagValue: (name, value) => { values.set(name, value); @@ -334,7 +330,6 @@ describe("registerFlag with built-in-named flags (r3323473227)", () => { ); const sink: ExtensionFlagSink = { getFlags: () => ExtensionRunner.aggregateFlags([ext]), - getToolNames: () => [], setFlagValue: (name, value) => { runtime.flagValues.set(name, value); }, diff --git a/packages/coding-agent/test/flag-tables.test.ts b/packages/coding-agent/test/flag-tables.test.ts index e24a071ba..50ff29365 100644 --- a/packages/coding-agent/test/flag-tables.test.ts +++ b/packages/coding-agent/test/flag-tables.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { parseArgs } from "../src/cli/args"; +import { parseArgs, validateToolNames } from "../src/cli/args"; import { OPTIONAL_VALUE_FLAGS, STRING_VALUE_FLAGS } from "../src/cli/flag-tables"; import { CliUsageError } from "../src/cli/usage-error"; @@ -92,16 +92,22 @@ describe("--tools validation", () => { expect(result.tools).toEqual(["grep", "glob"]); }); - it("defers unknown-name validation until extension discovery", () => { + it("defers unknown-name validation until all session tools are discovered", () => { expect(parseArgs(["--tools", "bash,intercom"]).tools).toEqual(["bash", "intercom"]); + expect(parseArgs(["--tools", "read,custom_tool"], new Map()).tools).toEqual(["read", "custom_tool"]); + }); +}); + +describe("--tools discovered-registry validation", () => { + it("accepts extension and custom tools after they enter the session registry", () => { + expect(() => + validateToolNames(["read", "intercom", "custom_tool"], ["read", "intercom", "custom_tool"]), + ).not.toThrow(); }); - it("accepts registered extension tools and still rejects unknown names after discovery", () => { - const extensionFlags = new Map(); - 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/, + it("rejects names absent from the final registry", () => { + expect(() => validateToolNames(["read", "missing"], ["read", "intercom", "custom_tool"])).toThrow( + /Unknown tool in --tools: missing/, ); }); });