diff --git a/open-sse/services/comboAgentMiddleware.ts b/open-sse/services/comboAgentMiddleware.ts index 02333cb145..1e97e9e096 100644 --- a/open-sse/services/comboAgentMiddleware.ts +++ b/open-sse/services/comboAgentMiddleware.ts @@ -34,7 +34,11 @@ interface Message { // ── Context Caching Tag ───────────────────────────────────────────────────── -const CACHE_TAG_PATTERN = /([^<]+)<\/omniModel>/; +// Handles both actual newlines (U+000A) and literal \n sequences injected +// by combo.ts streaming around the tag (#531). Non-global so that +// .exec() and .test() stay stateless; callers that need full replacement use +// String.prototype.replace() which replaces all non-overlapping matches. +const CACHE_TAG_PATTERN = /(?:\\n|\n)?([^<]+)<\/omniModel>(?:\\n|\n)?/; /** * Inject the model tag into the last assistant message (or append a new one). diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.tsx index 72ea8c9c83..3c86613ef8 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.tsx @@ -58,8 +58,10 @@ export default function ClaudeToolCard({ const effectiveConfigStatus = configStatus || batchStatus?.configStatus || null; useEffect(() => { + // (#523) Store the key *id* (not the masked string) so the backend can + // resolve the real secret from DB before writing to settings.json. if (apiKeys?.length > 0 && !selectedApiKey) { - setSelectedApiKey(apiKeys[0].key); + setSelectedApiKey(apiKeys[0].id); } }, [apiKeys, selectedApiKey]); @@ -95,10 +97,11 @@ export default function ClaudeToolCard({ } } }); - // Only set selectedApiKey if it exists in apiKeys list + // Restore selected key from file: match token stored in file against known keys const tokenFromFile = env.ANTHROPIC_AUTH_TOKEN; - if (tokenFromFile && apiKeys?.some((k) => k.key === tokenFromFile)) { - setSelectedApiKey(tokenFromFile); + if (tokenFromFile) { + const matchedKey = apiKeys?.find((k) => k.key === tokenFromFile); + if (matchedKey) setSelectedApiKey(matchedKey.id); } } }, [claudeStatus, apiKeys, tool.defaultModels, onModelMappingChange]); @@ -132,24 +135,27 @@ export default function ClaudeToolCard({ try { const env: any = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl() }; - // Get key from dropdown, fallback to first key or sk_omniroute for localhost - const keyToUse = - selectedApiKey?.trim() || - (apiKeys?.length > 0 ? apiKeys[0].key : null) || - (!cloudEnabled ? "sk_omniroute" : null); + // (#523) Prefer keyId lookup so the backend writes the real key to disk. + // Fall back to sk_omniroute for localhost-only setups without a key. + const selectedKeyId = selectedApiKey?.trim() || (apiKeys?.length > 0 ? apiKeys[0].id : null); + const skOmnirouteFallback = !cloudEnabled ? "sk_omniroute" : null; - if (keyToUse) { - env.ANTHROPIC_AUTH_TOKEN = keyToUse; + if (!selectedKeyId && skOmnirouteFallback) { + env.ANTHROPIC_AUTH_TOKEN = skOmnirouteFallback; } tool.defaultModels.forEach((model) => { const targetModel = modelMappings[model.alias]; if (targetModel && model.envKey) env[model.envKey] = targetModel; }); + + const postBody: Record = { env }; + if (selectedKeyId) postBody.keyId = selectedKeyId; + const res = await fetch("/api/cli-tools/claude-settings", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ env }), + body: JSON.stringify(postBody), }); const data = await res.json(); if (res.ok) { @@ -412,7 +418,7 @@ export default function ClaudeToolCard({ className="flex-1 px-2 py-1.5 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50" > {apiKeys.map((key) => ( - ))} diff --git a/src/app/api/cli-tools/claude-settings/route.ts b/src/app/api/cli-tools/claude-settings/route.ts index 5490296cbf..1c3dc32edc 100644 --- a/src/app/api/cli-tools/claude-settings/route.ts +++ b/src/app/api/cli-tools/claude-settings/route.ts @@ -12,6 +12,7 @@ import { createBackup } from "@/shared/services/backupService"; import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState"; import { cliSettingsEnvSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { getApiKeyById } from "@/lib/localDb"; // Get claude settings path based on OS const getClaudeSettingsPath = () => getCliPrimaryConfigPath("claude"); @@ -100,6 +101,22 @@ export async function POST(request: Request) { } 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; + if (keyId) { + try { + const keyRecord = await getApiKeyById(keyId); + if (keyRecord?.key) { + env.ANTHROPIC_AUTH_TOKEN = keyRecord.key as string; + } + } catch { + // Non-critical: fall back to whatever value was in env (e.g. sk_omniroute) + } + } + const settingsPath = getClaudeSettingsPath(); const claudeDir = path.dirname(settingsPath); diff --git a/src/app/api/cli-tools/cline-settings/route.ts b/src/app/api/cli-tools/cline-settings/route.ts index 8796065ea5..2c6fe656d3 100644 --- a/src/app/api/cli-tools/cline-settings/route.ts +++ b/src/app/api/cli-tools/cline-settings/route.ts @@ -9,6 +9,7 @@ import { createBackup } from "@/shared/services/backupService"; import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState"; import { cliModelConfigSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { getApiKeyById } from "@/lib/localDb"; const CLINE_DATA_DIR = path.join(os.homedir(), ".cline", "data"); const GLOBAL_STATE_PATH = path.join(CLINE_DATA_DIR, "globalState.json"); @@ -125,7 +126,18 @@ export async function POST(request: Request) { if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } - const { baseUrl, apiKey, model } = validation.data; + 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; + if (keyId) { + try { + const keyRecord = await getApiKeyById(keyId); + if (keyRecord?.key) apiKey = keyRecord.key as string; + } catch { + /* non-critical */ + } + } // Ensure directory exists await fs.mkdir(CLINE_DATA_DIR, { recursive: true }); diff --git a/src/app/api/cli-tools/openclaw-settings/route.ts b/src/app/api/cli-tools/openclaw-settings/route.ts index 510e7fd21f..e8dbebbe77 100644 --- a/src/app/api/cli-tools/openclaw-settings/route.ts +++ b/src/app/api/cli-tools/openclaw-settings/route.ts @@ -12,6 +12,7 @@ import { createBackup } from "@/shared/services/backupService"; import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState"; import { cliModelConfigSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { getApiKeyById } from "@/lib/localDb"; const getOpenClawSettingsPath = () => getCliPrimaryConfigPath("openclaw"); const getOpenClawDir = () => path.dirname(getOpenClawSettingsPath()); @@ -101,7 +102,18 @@ export async function POST(request: Request) { if (isValidationFailure(validation)) { return NextResponse.json({ error: validation.error }, { status: 400 }); } - const { baseUrl, apiKey, model } = validation.data; + 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; + if (keyId) { + try { + const keyRecord = await getApiKeyById(keyId); + if (keyRecord?.key) apiKey = keyRecord.key as string; + } catch { + /* non-critical */ + } + } const openclawDir = getOpenClawDir(); const settingsPath = getOpenClawSettingsPath();