From 5028ea697341cb55073deb68c47fec36ddc78e3c Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 9 Sep 2026 13:28:24 +0700 Subject: [PATCH] config: warn on unknown toolSets typo instead of dropping silently unknownToolSetNames() + startup notice (stderr headless, NoticeBus interactive) flags gti vs git typos at boot, not as that set off. --- TODO.md | 2 -- src/cli.tsx | 10 +++++++++- src/config.ts | 5 +++++ test/config-toolsets.test.ts | 10 ++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 test/config-toolsets.test.ts diff --git a/TODO.md b/TODO.md index 577f7d5..dd8cd2f 100644 --- a/TODO.md +++ b/TODO.md @@ -100,8 +100,6 @@ Not bugs exactly, but things that will bite someone. injected. `/memory` may merge them, or may keep both. - **Windows `cmd /c` differs from `bash -lc`.** A command the model writes for one shell may fail on the other. The prompt states the platform; it does not translate. -- **An unknown name in `toolSets` is dropped silently.** The header line shows which sets - actually loaded, but a typo reads as "that set is off" rather than as a mistake. - **Permission rules gate the call, not what it does.** `bash` with `git *` allowed will run a `git` alias that shells out to anything, and there is no sandbox around the shell. Codex solves this with OS-level isolation — Seatbelt, Landlock, a Windows equivalent — which is three diff --git a/src/cli.tsx b/src/cli.tsx index 56122af..39bbd9a 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -4,7 +4,7 @@ import React from 'react'; import type { LanguageModel, ModelMessage } from 'ai'; import { resolveAgent, VARIANTS, isThinkingLevel, type AgentVariant } from './agents'; import { loadExternalPlugins, loadExternalTools } from './autoload'; -import { configPath, loadConfig, missingKeyMessage, resolveModel, writeConfigFile, type Config } from './config'; +import { configPath, loadConfig, missingKeyMessage, readConfigFile, resolveModel, unknownToolSetNames, writeConfigFile, type Config } from './config'; import type { FallbackEvent } from './fallback'; import { farewell } from './farewell'; import { readStdin, runHeadless } from './headless'; @@ -107,6 +107,8 @@ if (modelFlag) process.env['SHIRO_MODEL'] = modelFlag; if (baseUrlFlag) process.env['SHIRO_BASE_URL'] = baseUrlFlag; let cfg = await loadConfig(); +const rawToolSets = (await readConfigFile()).toolSets as unknown; +const unknownToolSets = unknownToolSetNames(rawToolSets); const yolo = has('--yolo'); const headless = flag('-p', '--print') !== undefined; @@ -121,6 +123,12 @@ const notices = createNoticeBus(); const subagents = createSubagentBus(); const askBridge = createAskBridge(); +if (unknownToolSets.length > 0) { + const msg = `unknown toolSets ignored: ${unknownToolSets.join(', ')} — valid: core, edit-plus, nav, extra, git, net`; + if (headless) process.stderr.write(`shiro: ${msg}\n`); + else notices.emit(msg); +} + function reportFallback(e: FallbackEvent): void { const line = `endpoint fallback: ${e.from} rejected the request, retrying on ${e.to}\n ${e.reason}`; if (headless) process.stderr.write(`shiro: ${line}\n`); diff --git a/src/config.ts b/src/config.ts index 08e858f..07e13fc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -82,6 +82,11 @@ export async function writeConfigFile(patch: Partial): Promise { } /** File config, then env overrides. Env wins so `SHIRO_MODEL=x shiro` works. */ +export function unknownToolSetNames(raw: unknown): string[] { + if (!Array.isArray(raw)) return []; + return (raw as unknown[]).filter((v): v is string => typeof v === 'string' && !isToolSetName(v)); +} + export async function loadConfig(): Promise { const file = await readConfigFile(); diff --git a/test/config-toolsets.test.ts b/test/config-toolsets.test.ts new file mode 100644 index 0000000..ee0335d --- /dev/null +++ b/test/config-toolsets.test.ts @@ -0,0 +1,10 @@ +import { expect, test } from 'bun:test'; +import { unknownToolSetNames } from '../src/config'; + +test('unknownToolSetNames flags typos and ignores valid ones', () => { + expect(unknownToolSetNames(['edit-plus', 'gti', 'net', 'extra'])).toEqual(['gti']); + expect(unknownToolSetNames(['core', 'git'])).toEqual([]); + expect(unknownToolSetNames(undefined)).toEqual([]); + expect(unknownToolSetNames([])).toEqual([]); + expect(unknownToolSetNames(null)).toEqual([]); +});