fix: prune Kilo provider models in configure flow

This commit is contained in:
Gustavo Madeira Santana
2026-02-23 20:19:40 -05:00
parent 032ae4cb8b
commit f7a320514c
5 changed files with 94 additions and 3 deletions

View File

@@ -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.

View File

@@ -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",
]);
});
});

View File

@@ -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);
}
}

View File

@@ -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([]);
});
});

View File

@@ -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<string>();
if (selectedKilocodeIds.size === 0 && !(opts?.pruneWhenNoKilocodeSelection ?? false)) {
return cfg;
}
let mutated = false;