Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/strict-config-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code-sdk": minor
"@moonshot-ai/kimi-code": minor
---

Add a `--strict-config` CLI flag that rejects unknown keys in `config.toml`.
4 changes: 3 additions & 1 deletion apps/kimi-code/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export function createProgram(
)
.addOption(new Option('--yes').hideHelp().default(false))
.addOption(new Option('--auto-approve').hideHelp().default(false))
.option('--plan', 'Start in plan mode.', false);
.option('--plan', 'Start in plan mode.', false)
.option('--strict-config', 'Reject unknown keys in config.toml.', false);

registerExportCommand(program);
registerProviderCommand(program);
Expand Down Expand Up @@ -119,6 +120,7 @@ export function createProgram(
outputFormat: raw['outputFormat'] as CLIOptions['outputFormat'],
prompt: raw['prompt'] as string | undefined,
skillsDirs: raw['skillsDir'] as string[],
strictConfig: raw['strictConfig'] as boolean,
};

onMain(opts);
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface CLIOptions {
outputFormat: PromptOutputFormat | undefined;
prompt: string | undefined;
skillsDirs: string[];
strictConfig: boolean;
}

export interface ValidatedOptions {
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/cli/run-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export async function runPrompt(
uiMode: PROMPT_UI_MODE,
skillDirs: opts.skillsDirs,
telemetry: telemetryClient,
strictConfig: opts.strictConfig,
onOAuthRefresh: (outcome) => {
if (outcome.success) {
track('oauth_refresh', { success: true });
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/cli/run-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export async function runShell(
homeDir: telemetryBootstrap.homeDir,
identity: createKimiCodeHostIdentity(version),
telemetry: telemetryClient,
strictConfig: opts.strictConfig,
onOAuthRefresh: (outcome) => {
if (outcome.success) {
track('oauth_refresh', { success: true });
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ const MIGRATE_CLI_OPTIONS: CLIOptions = {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/cli/goal-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ function opts(overrides: Partial<Parameters<typeof runPrompt>[0]> = {}) {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: '/goal Ship feature X',
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/cli/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ function defaultOpts(): CLIOptions {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
10 changes: 10 additions & 0 deletions apps/kimi-code/test/cli/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ describe('CLI options parsing', () => {
});
});

describe('--strict-config', () => {
it('defaults to false', () => {
expect(parse([]).strictConfig).toBe(false);
});

it('sets strict config flag', () => {
expect(parse(['--strict-config']).strictConfig).toBe(true);
});
});

describe('--auto / --yolo / --plan with --session / --continue', () => {
it('allows --auto with --continue', () => {
const opts = parse(['--auto', '--continue']);
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/cli/run-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ function opts(overrides: Partial<Parameters<typeof runPrompt>[0]> = {}) {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: 'say hello',
Expand Down
11 changes: 11 additions & 0 deletions apps/kimi-code/test/cli/run-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ describe('runShell', () => {
yolo: true,
auto: false,
plan: true,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -265,6 +266,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -305,6 +307,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -343,6 +346,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -381,6 +385,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -414,6 +419,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -465,6 +471,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -503,6 +510,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -533,6 +541,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -570,6 +579,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -623,6 +633,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/tui/activity-pane.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function makeStartupInput(): KimiTUIStartupInput {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ function makeStartupInput(): KimiTUIStartupInput {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/tui/kimi-tui-startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ function makeStartupInput(
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/tui/message-replay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function makeStartupInput(): KimiTUIStartupInput {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/test/tui/signal-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function makeStartupInput(): KimiTUIStartupInput {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down
19 changes: 19 additions & 0 deletions packages/agent-core/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ export const MoonshotServiceConfigSchema = z.object({

export type MoonshotServiceConfig = z.infer<typeof MoonshotServiceConfigSchema>;

export const ServicesConfigStrictSchema = z.object({
moonshotSearch: MoonshotServiceConfigSchema.strict().optional(),
moonshotFetch: MoonshotServiceConfigSchema.strict().optional(),
}).strict();

export type ServicesConfigStrict = z.infer<typeof ServicesConfigStrictSchema>;

export const ServicesConfigSchema = z.object({
moonshotSearch: MoonshotServiceConfigSchema.optional(),
moonshotFetch: MoonshotServiceConfigSchema.optional(),
Expand Down Expand Up @@ -208,6 +215,18 @@ export const KimiConfigSchema = z.object({
raw: z.record(z.string(), z.unknown()).optional(),
});

/**
* Strict variant used by `--strict-config`. Rejects unknown keys at the
* section level while keeping provider/model aliases open for custom fields.
*/
export const KimiConfigStrictSchema = KimiConfigSchema.extend({
permission: PermissionConfigSchema.strict().optional(),
services: ServicesConfigStrictSchema.optional(),
loopControl: LoopControlSchema.strict().optional(),
background: BackgroundConfigSchema.strict().optional(),
thinking: ThinkingConfigSchema.strict().optional(),
}).strict();

export type KimiConfig = z.infer<typeof KimiConfigSchema>;

const ProviderConfigPatchSchema = ProviderConfigSchema.partial();
Expand Down
41 changes: 31 additions & 10 deletions packages/agent-core/src/config/toml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ErrorCodes, KimiError } from '#/errors';
import { applyEnvModelConfig, stripEnvModelConfig } from './env-model';
import {
KimiConfigSchema,
KimiConfigStrictSchema,
formatConfigValidationError,
getDefaultConfig,
type BackgroundConfig,
Expand Down Expand Up @@ -62,12 +63,19 @@ export async function ensureConfigFile(filePath: string): Promise<void> {
}
}

export function readConfigFile(filePath: string): KimiConfig {
export interface ReadConfigOptions {
readonly strict?: boolean;
}

export function readConfigFile(
filePath: string,
options: ReadConfigOptions = {},
): KimiConfig {
if (!existsSync(filePath)) {
return getDefaultConfig();
}
const text = readFileSync(filePath, 'utf-8');
return parseConfigString(text, filePath);
return parseConfigString(text, filePath, options);
}

/**
Expand All @@ -76,9 +84,12 @@ export function readConfigFile(filePath: string): KimiConfig {
* sections). Re-throws validation failures with a short actionable message —
* UIs surface it directly — instead of the raw validation details.
*/
export function readConfigFileForUpdate(filePath: string): KimiConfig {
export function readConfigFileForUpdate(
filePath: string,
options: ReadConfigOptions = {},
): KimiConfig {
try {
return readConfigFile(filePath);
return readConfigFile(filePath, options);
} catch (error) {
if (error instanceof KimiError && error.code === ErrorCodes.CONFIG_INVALID) {
throw new KimiError(
Expand All @@ -100,8 +111,9 @@ export function readConfigFileForUpdate(filePath: string): KimiConfig {
export function loadRuntimeConfig(
filePath: string,
env: Readonly<Record<string, string | undefined>> = process.env,
options: ReadConfigOptions = {},
): KimiConfig {
return applyEnvModelConfig(readConfigFile(filePath), env);
return applyEnvModelConfig(readConfigFile(filePath, options), env);
}

export interface RuntimeConfigLoadResult {
Expand Down Expand Up @@ -262,7 +274,11 @@ function describeTomlSyntaxError(error: unknown): string {
return firstLine;
}

export function parseConfigString(tomlText: string, filePath = 'config.toml'): KimiConfig {
export function parseConfigString(
tomlText: string,
filePath = 'config.toml',
options: ReadConfigOptions = {},
): KimiConfig {
if (tomlText.trim().length === 0) {
return getDefaultConfig();
}
Expand All @@ -276,16 +292,21 @@ export function parseConfigString(tomlText: string, filePath = 'config.toml'): K
});
}

return parseConfigData(data, filePath);
return parseConfigData(data, filePath, options);
}

function parseConfigData(data: Record<string, unknown>, filePath: string): KimiConfig {
function parseConfigData(
data: Record<string, unknown>,
filePath: string,
options: ReadConfigOptions = {},
): KimiConfig {
const raw = cloneRecord(data);
const transformed = transformTomlData(data);
transformed['raw'] = raw;

const schema = options.strict ? KimiConfigStrictSchema : KimiConfigSchema;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unknown top-level tables in strict config

When --strict-config is used, this parses the already-transformed object, but transformTomlData drops any unrecognized top-level table because object-valued keys only get copied for known sections. As a result, a config like [typo]\nkey = "value" is silently accepted even though the flag is meant to reject unknown top-level keys; validate the raw top-level keys or carry unknown table sections through before applying the strict schema.

Useful? React with 👍 / 👎.

try {
return KimiConfigSchema.parse(transformed);
return schema.parse(transformed);
} catch (error) {
throw new KimiError(ErrorCodes.CONFIG_INVALID, `Invalid configuration in ${filePath}: ${formatConfigValidationError(error)}`, {
cause: error,
Expand Down Expand Up @@ -364,7 +385,7 @@ function transformModelData(data: Record<string, unknown>): Record<string, unkno

function transformPermissionData(data: Record<string, unknown>): Record<string, unknown> {
const raw = transformPlainObject(data);
const out: Record<string, unknown> = {};
const out: Record<string, unknown> = { ...raw };

const rules: unknown[] = [];
appendPermissionRules(rules, raw['rules']);
Expand Down
Loading