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
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 @@ -60,6 +60,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 @@ -142,6 +142,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 @@ -161,6 +161,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('--model / -m', () => {
it('parses -m as a model override', () => {
expect(parse(['-m', 'kimi-code/k2']).model).toBe('kimi-code/k2');
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 @@ -129,6 +129,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
10 changes: 10 additions & 0 deletions apps/kimi-code/test/cli/run-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ describe('runShell', () => {
yolo: true,
auto: false,
plan: true,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -263,6 +264,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -303,6 +305,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -341,6 +344,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -379,6 +383,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -412,6 +417,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -463,6 +469,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -499,6 +506,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -536,6 +544,7 @@ describe('runShell', () => {
yolo: false,
auto: false,
plan: false,
strictConfig: false,
model: undefined,
outputFormat: undefined,
prompt: undefined,
Expand Down Expand Up @@ -589,6 +598,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 @@ -75,6 +75,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
34 changes: 26 additions & 8 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 @@ -79,11 +87,16 @@ export function readConfigFile(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 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 @@ -97,16 +110,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 @@ -185,7 +203,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
15 changes: 10 additions & 5 deletions packages/agent-core/src/rpc/core-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export interface KimiCoreOptions {
readonly skillDirs?: readonly string[];
readonly telemetry?: TelemetryClient | undefined;
readonly appVersion?: string;
readonly strictConfig?: boolean;
}

export class KimiCore implements PromisableMethods<CoreAPI> {
Expand All @@ -140,6 +141,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
private pluginsLoadError: Error | undefined;
private readonly appVersion: string | undefined;
private readonly experimentalFlags: FlagResolver;
private readonly strictConfig: boolean;

constructor(
protected readonly rpcClient: CoreRPCClient,
Expand All @@ -158,8 +160,11 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
this.skillDirs = options.skillDirs ?? [];
this.telemetry = options.telemetry ?? noopTelemetryClient;
this.appVersion = options.appVersion;
this.strictConfig = options.strictConfig ?? false;
ensureKimiHome(this.homeDir);
this.config = loadRuntimeConfig(this.configPath);
this.config = loadRuntimeConfig(this.configPath, process.env, {
strict: this.strictConfig,
});
this.experimentalFlags = new FlagResolver(
process.env,
FLAG_DEFINITIONS,
Expand Down Expand Up @@ -447,15 +452,15 @@ export class KimiCore implements PromisableMethods<CoreAPI> {

async getKimiConfig(input?: GetKimiConfigPayload): Promise<KimiConfig> {
if (input?.reload) {
this.setRuntimeConfig(loadRuntimeConfig(this.configPath));
this.setRuntimeConfig(loadRuntimeConfig(this.configPath, process.env, { strict: this.strictConfig }));
}
return this.config;
}

async setKimiConfig(input: SetKimiConfigPayload): Promise<KimiConfig> {
const config = mergeConfigPatch(readConfigFile(this.configPath), input);
await writeConfigFile(this.configPath, config);
return this.setRuntimeConfig(loadRuntimeConfig(this.configPath));
return this.setRuntimeConfig(loadRuntimeConfig(this.configPath, process.env, { strict: this.strictConfig }));
}

async removeKimiProvider(input: RemoveKimiProviderPayload): Promise<KimiConfig> {
Expand Down Expand Up @@ -486,7 +491,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
}

await writeConfigFile(this.configPath, config);
return this.setRuntimeConfig(loadRuntimeConfig(this.configPath));
return this.setRuntimeConfig(loadRuntimeConfig(this.configPath, process.env, { strict: this.strictConfig }));
}

prompt({ sessionId, ...payload }: SessionAgentPayload<PromptPayload>) {
Expand Down Expand Up @@ -860,7 +865,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
}

private reloadProviderManager(): KimiConfig {
return this.setRuntimeConfig(loadRuntimeConfig(this.configPath));
return this.setRuntimeConfig(loadRuntimeConfig(this.configPath, process.env, { strict: this.strictConfig }));
}

private setRuntimeConfig(config: KimiConfig): KimiConfig {
Expand Down
Loading