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.
This commit is contained in:
asepharyana
2026-09-09 13:28:24 +07:00
parent dcede3c10a
commit 5028ea6973
4 changed files with 24 additions and 3 deletions
-2
View File
@@ -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
+9 -1
View File
@@ -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`);
+5
View File
@@ -82,6 +82,11 @@ export async function writeConfigFile(patch: Partial<Config>): Promise<string> {
}
/** 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<Config> {
const file = await readConfigFile();
+10
View File
@@ -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([]);
});