From 8c36d6e9aa2c8478d74919801012df5c44ae6958 Mon Sep 17 00:00:00 2001 From: benzntech Date: Wed, 15 Apr 2026 11:25:19 +0530 Subject: [PATCH] fix(cli-tools): resolve API key resolution and model mapping bugs in CLI tools Two critical bugs prevented the Dashboard CLI Tools feature from working: Bug #1: keyId stripped by Zod validation - Zod's safeParse() stripped the keyId field before backend could read it - The real API key was never resolved from DB and written to settings.json - Fix: Extract keyId from rawBody BEFORE validation in all 6 CLI settings routes - Affected: claude, codex, droid, cline, openclaw, kilo settings routes Bug #2: Key matching logic compared full key vs masked key - Frontend compared full key (sk-31c4ea6a...) against masked key (sk-31c4****dfa6) - They never matched, so the dropdown never showed the correct selected key - Fix: Mask the token from file before comparing against the masked API keys list - Affected: ClaudeToolCard.tsx Also fixes model mapping being scrambled when key resolution failed. Co-authored-by: Qwen-Coder --- .../cli-tools/components/ClaudeToolCard.tsx | 5 ++++- src/app/api/cli-tools/claude-settings/route.ts | 12 +++++++----- src/app/api/cli-tools/cline-settings/route.ts | 6 ++++-- src/app/api/cli-tools/codex-settings/route.ts | 10 ++++++---- src/app/api/cli-tools/droid-settings/route.ts | 6 ++++-- src/app/api/cli-tools/kilo-settings/route.ts | 6 ++++-- src/app/api/cli-tools/openclaw-settings/route.ts | 6 ++++-- src/app/api/providers/[id]/models/route.ts | 5 ++++- 8 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.tsx index 3c86613ef8ad..968880976bf5 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.tsx @@ -100,7 +100,10 @@ export default function ClaudeToolCard({ // Restore selected key from file: match token stored in file against known keys const tokenFromFile = env.ANTHROPIC_AUTH_TOKEN; if (tokenFromFile) { - const matchedKey = apiKeys?.find((k) => k.key === tokenFromFile); + // (#523) Keys from /api/keys are masked (first 8 + "****" + last 4). + // Mask the token from file to compare against the masked list. + const maskedToken = tokenFromFile.slice(0, 8) + "****" + tokenFromFile.slice(-4); + const matchedKey = apiKeys?.find((k) => k.key === maskedToken); if (matchedKey) setSelectedApiKey(matchedKey.id); } } diff --git a/src/app/api/cli-tools/claude-settings/route.ts b/src/app/api/cli-tools/claude-settings/route.ts index 1c3dc32edcb9..63205c9e2a54 100644 --- a/src/app/api/cli-tools/claude-settings/route.ts +++ b/src/app/api/cli-tools/claude-settings/route.ts @@ -95,17 +95,19 @@ export async function POST(request: Request) { return NextResponse.json({ error: writeGuard }, { status: 403 }); } + // (#523/#526) Extract keyId BEFORE validation — Zod strips unknown fields! + // The /api/keys list endpoint returns masked key strings — sending those to + // disk would save an unusable half-hidden token. Resolving by ID guarantees + // we always write the full key value to the config file. + const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + const validation = validateBody(cliSettingsEnvSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } const { env } = validation.data; - // (#523/#526) If a keyId was provided, resolve the real API key from DB. - // The /api/keys list endpoint returns masked key strings — sending those to - // disk would save an unusable half-hidden token. Resolving by ID guarantees - // we always write the full key value to the config file. - const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + // Resolve the real API key from DB by ID if (keyId) { try { const keyRecord = await getApiKeyById(keyId); diff --git a/src/app/api/cli-tools/cline-settings/route.ts b/src/app/api/cli-tools/cline-settings/route.ts index 2c6fe656d360..b032babb9351 100644 --- a/src/app/api/cli-tools/cline-settings/route.ts +++ b/src/app/api/cli-tools/cline-settings/route.ts @@ -122,14 +122,16 @@ export async function POST(request: Request) { return NextResponse.json({ error: writeGuard }, { status: 403 }); } + // (#526) Extract keyId BEFORE validation — Zod strips unknown fields! + const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + const validation = validateBody(cliModelConfigSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } let { baseUrl, apiKey, model } = validation.data; - // (#526) Resolve real key from DB if keyId was provided - const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + // Resolve real key from DB by ID if (keyId) { try { const keyRecord = await getApiKeyById(keyId); diff --git a/src/app/api/cli-tools/codex-settings/route.ts b/src/app/api/cli-tools/codex-settings/route.ts index 6067f5232661..ccdfa752d94c 100644 --- a/src/app/api/cli-tools/codex-settings/route.ts +++ b/src/app/api/cli-tools/codex-settings/route.ts @@ -163,6 +163,11 @@ export async function POST(request: Request) { return NextResponse.json({ error: writeGuard }, { status: 403 }); } + // (#549) Extract keyId BEFORE validation — Zod strips unknown fields! + // The dashboard sends masked key strings — resolving by ID guarantees + // we always write the full key value to the config file. + const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + const validation = validateBody(cliModelConfigSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); @@ -176,10 +181,7 @@ export async function POST(request: Request) { ); } - // (#549) Resolve real key from DB if keyId was provided. - // The dashboard sends masked key strings — resolving by ID guarantees - // we always write the full key value to the config file. - const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + // Resolve real key from DB by ID if (keyId) { try { const keyRecord = await getApiKeyById(keyId); diff --git a/src/app/api/cli-tools/droid-settings/route.ts b/src/app/api/cli-tools/droid-settings/route.ts index 3e3be5fdb413..25cd6d52cc4d 100644 --- a/src/app/api/cli-tools/droid-settings/route.ts +++ b/src/app/api/cli-tools/droid-settings/route.ts @@ -98,6 +98,9 @@ export async function POST(request: Request) { return NextResponse.json({ error: writeGuard }, { status: 403 }); } + // (#549) Extract keyId BEFORE validation — Zod strips unknown fields! + const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + const validation = validateBody(cliModelConfigSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); @@ -105,8 +108,7 @@ export async function POST(request: Request) { const { baseUrl, model } = validation.data; let { apiKey } = validation.data; - // (#549) Resolve real key from DB if keyId was provided. - const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + // Resolve real key from DB by ID if (keyId) { try { const keyRecord = await getApiKeyById(keyId); diff --git a/src/app/api/cli-tools/kilo-settings/route.ts b/src/app/api/cli-tools/kilo-settings/route.ts index 0974ee5455ec..95248e313978 100644 --- a/src/app/api/cli-tools/kilo-settings/route.ts +++ b/src/app/api/cli-tools/kilo-settings/route.ts @@ -130,6 +130,9 @@ export async function POST(request) { return NextResponse.json({ error: writeGuard }, { status: 403 }); } + // (#549) Extract keyId BEFORE validation — Zod strips unknown fields! + const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + const validation = validateBody(cliModelConfigSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); @@ -137,8 +140,7 @@ export async function POST(request) { const { baseUrl, model } = validation.data; let { apiKey } = validation.data; - // (#549) Resolve real key from DB if keyId was provided. - const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + // Resolve real key from DB by ID if (keyId) { try { const keyRecord = await getApiKeyById(keyId); diff --git a/src/app/api/cli-tools/openclaw-settings/route.ts b/src/app/api/cli-tools/openclaw-settings/route.ts index e8dbebbe776e..88958bb5b798 100644 --- a/src/app/api/cli-tools/openclaw-settings/route.ts +++ b/src/app/api/cli-tools/openclaw-settings/route.ts @@ -98,14 +98,16 @@ export async function POST(request: Request) { return NextResponse.json({ error: writeGuard }, { status: 403 }); } + // (#526) Extract keyId BEFORE validation — Zod strips unknown fields! + const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + const validation = validateBody(cliModelConfigSchema, rawBody); if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } let { baseUrl, apiKey, model } = validation.data; - // (#526) Resolve real key from DB if keyId was provided - const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; + // Resolve real key from DB by ID if (keyId) { try { const keyRecord = await getApiKeyById(keyId); diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 09d05ecbc00d..e6f75c506968 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -623,7 +623,10 @@ export async function GET( baseUrl = baseUrl.slice(0, -9); } - const url = `${baseUrl}/models`; + // Use modelsPath from provider node if available, otherwise default to /models + const psd = asRecord(connection.providerSpecificData); + const modelsPath = toNonEmptyString(psd.modelsPath) || "/models"; + const url = `${baseUrl}${modelsPath}`; const token = accessToken || apiKey; const response = await runWithProxyContext(proxy, () => fetch(url, {