From f7a320514cc3db0fb43e2927e5034d61e2ca4b38 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 23 Feb 2026 20:19:40 -0500 Subject: [PATCH] fix: prune Kilo provider models in configure flow --- CHANGELOG.md | 1 + ...re.gateway-auth.prompt-auth-config.test.ts | 60 +++++++++++++++++++ src/commands/configure.gateway-auth.ts | 6 +- src/commands/model-picker.test.ts | 23 +++++++ src/commands/model-picker.ts | 7 ++- 5 files changed, 94 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 239d38a1442..09bc415a32f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Config/Kilo Gateway: in `openclaw configure`, prune persisted `models.providers.kilocode.models` to selected Kilo allowlist entries (including empty Kilo selections) only during Kilo auth flow, preserving non-Kilo auth behavior. (#24921) thanks @gumadeiras. - Security/Config writes: block reserved prototype keys in account-id normalization and route account config resolution through own-key lookups, hardening `/allowlist` and account-scoped config paths against prototype-chain pollution. - Security/Exec: harden `safeBins` long-option validation by rejecting unknown/ambiguous GNU long-option abbreviations and denying sort filesystem-dependent flags (`--random-source`, `--temporary-directory`, `-T`), closing safe-bin denylist bypasses. Thanks @jiseoung. - Security/Channels: unify dangerous name-matching policy checks (`dangerouslyAllowNameMatching`) across core and extension channels, share mutable-allowlist detectors between `openclaw doctor` and `openclaw security audit`, and scan all configured accounts (not only the default account) in channel security audit findings. diff --git a/src/commands/configure.gateway-auth.prompt-auth-config.test.ts b/src/commands/configure.gateway-auth.prompt-auth-config.test.ts index 26ebe1e3d73..0f2c1255aa2 100644 --- a/src/commands/configure.gateway-auth.prompt-auth-config.test.ts +++ b/src/commands/configure.gateway-auth.prompt-auth-config.test.ts @@ -128,4 +128,64 @@ describe("promptAuthConfig", () => { "MiniMax-M2.1", ]); }); + + it("prunes Kilo provider models to empty when Kilo auth selection has no Kilo models", async () => { + mocks.promptAuthChoiceGrouped.mockResolvedValue("kilocode-api-key"); + mocks.applyAuthChoice.mockResolvedValue({ + config: { + agents: { + defaults: { + model: { primary: "kilocode/anthropic/claude-opus-4.6" }, + }, + }, + models: { + providers: { + kilocode: { + baseUrl: "https://api.kilo.ai/api/gateway/", + api: "openai-completions", + models: [ + { id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" }, + { id: "minimax/minimax-m2.5:free", name: "MiniMax M2.5 (Free)" }, + ], + }, + }, + }, + }, + }); + mocks.promptModelAllowlist.mockResolvedValue({ + models: ["openai/gpt-5.2"], + }); + + const result = await promptAuthConfig({}, makeRuntime(), noopPrompter); + expect(result.models?.providers?.kilocode?.models).toEqual([]); + }); + + it("does not prune Kilo provider models outside Kilo auth flow", async () => { + mocks.promptAuthChoiceGrouped.mockResolvedValue("token"); + mocks.applyAuthChoice.mockResolvedValue({ + config: { + models: { + providers: { + kilocode: { + baseUrl: "https://api.kilo.ai/api/gateway/", + api: "openai-completions", + models: [ + { id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" }, + { id: "minimax/minimax-m2.5:free", name: "MiniMax M2.5 (Free)" }, + ], + }, + }, + }, + }, + }); + mocks.promptModelAllowlist.mockResolvedValue({ + models: ["openai/gpt-5.2"], + }); + + const result = await promptAuthConfig({}, makeRuntime(), noopPrompter); + expect(result.models?.providers?.kilocode?.models?.map((model) => model.id)).toEqual([ + "anthropic/claude-opus-4.6", + "minimax/minimax-m2.5:free", + ]); + }); }); diff --git a/src/commands/configure.gateway-auth.ts b/src/commands/configure.gateway-auth.ts index 479f9e7d82d..278b8191d7a 100644 --- a/src/commands/configure.gateway-auth.ts +++ b/src/commands/configure.gateway-auth.ts @@ -127,7 +127,11 @@ export async function promptAuthConfig( }); if (allowlistSelection.models) { next = applyModelAllowlist(next, allowlistSelection.models); - next = pruneKilocodeProviderModelsToAllowlist(next, allowlistSelection.models); + if (authChoice === "kilocode-api-key") { + next = pruneKilocodeProviderModelsToAllowlist(next, allowlistSelection.models, { + pruneWhenNoKilocodeSelection: true, + }); + } next = applyModelFallbacksFromSelection(next, allowlistSelection.models); } } diff --git a/src/commands/model-picker.test.ts b/src/commands/model-picker.test.ts index 7ea42e5d39f..546c66ae132 100644 --- a/src/commands/model-picker.test.ts +++ b/src/commands/model-picker.test.ts @@ -318,4 +318,27 @@ describe("pruneKilocodeProviderModelsToAllowlist", () => { "MiniMax-M2.5", ]); }); + + it("can prune Kilo provider models to empty when configured", () => { + const config = { + models: { + providers: { + kilocode: { + baseUrl: "https://api.kilo.ai/api/gateway/", + api: "openai-completions", + models: [ + makeProviderModel("anthropic/claude-opus-4.6", "Claude Opus 4.6"), + makeProviderModel("minimax/minimax-m2.5:free", "MiniMax M2.5 (Free)"), + ], + }, + }, + }, + } as OpenClawConfig; + + const next = pruneKilocodeProviderModelsToAllowlist(config, ["openai/gpt-5.2"], { + pruneWhenNoKilocodeSelection: true, + }); + + expect(next.models?.providers?.kilocode?.models).toEqual([]); + }); }); diff --git a/src/commands/model-picker.ts b/src/commands/model-picker.ts index c843d637241..f4bf857880c 100644 --- a/src/commands/model-picker.ts +++ b/src/commands/model-picker.ts @@ -552,6 +552,9 @@ export function applyModelAllowlist(cfg: OpenClawConfig, models: string[]): Open export function pruneKilocodeProviderModelsToAllowlist( cfg: OpenClawConfig, selectedModels: string[], + opts?: { + pruneWhenNoKilocodeSelection?: boolean; + }, ): OpenClawConfig { const normalized = normalizeModelKeys(selectedModels); if (normalized.length === 0) { @@ -564,8 +567,8 @@ export function pruneKilocodeProviderModelsToAllowlist( const selectedByProvider = selectedModelIdsByProvider(normalized); // Keep this scoped to Kilo Gateway: do not mutate other providers here. - const selectedKilocodeIds = selectedByProvider.get("kilocode"); - if (!selectedKilocodeIds || selectedKilocodeIds.size === 0) { + const selectedKilocodeIds = selectedByProvider.get("kilocode") ?? new Set(); + if (selectedKilocodeIds.size === 0 && !(opts?.pruneWhenNoKilocodeSelection ?? false)) { return cfg; } let mutated = false;