Skip to content

Commit 4e00da0

Browse files
committed
fix(agent): preserve local cli credentials
1 parent 1dea9a3 commit 4e00da0

12 files changed

Lines changed: 658 additions & 38 deletions

src/main/libs/claudeSettings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,10 +536,22 @@ export function resolveAllProviderApiKeys(): Record<string, string> {
536536
export function buildEnvForConfig(config: CoworkApiConfig): Record<string, string> {
537537
const baseEnv = { ...process.env } as Record<string, string>;
538538

539+
baseEnv.WESIGHT_APIKEY_ACTIVE_PROVIDER = config.apiKey;
540+
baseEnv.LOBSTER_PROVIDER_API_KEY = config.apiKey;
541+
539542
baseEnv.ANTHROPIC_AUTH_TOKEN = config.apiKey;
540543
baseEnv.ANTHROPIC_API_KEY = config.apiKey;
541544
baseEnv.ANTHROPIC_BASE_URL = config.baseURL;
542545
baseEnv.ANTHROPIC_MODEL = config.model;
546+
baseEnv.ANTHROPIC_REASONING_MODEL = config.model;
547+
baseEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = config.model;
548+
baseEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = config.model;
549+
baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = config.model;
550+
baseEnv.ANTHROPIC_SMALL_FAST_MODEL = config.model;
551+
552+
baseEnv.OPENAI_API_KEY = config.apiKey;
553+
baseEnv.OPENAI_BASE_URL = config.baseURL;
554+
baseEnv.OPENAI_MODEL = config.model;
543555
return baseEnv;
544556
}
545557

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { expect, test } from 'vitest';
2+
3+
import { buildEnvForConfig } from './claudeSettings';
4+
import {
5+
mergeClaudeSettingsForWesightModel,
6+
mergeCodexConfigForWesightModel,
7+
} from './externalAgentConfigSync';
8+
9+
const apiConfig = {
10+
apiKey: 'sk-wesight-secret',
11+
baseURL: 'https://api.example.com/v1',
12+
model: 'glm-5.1-highspeed',
13+
apiType: 'openai' as const,
14+
};
15+
16+
test('mergeCodexConfigForWesightModel preserves user TOML content', () => {
17+
const existing = [
18+
'# user comment',
19+
'[features]',
20+
'web_search_request = true',
21+
'',
22+
'[model_providers.local]',
23+
'name = "local"',
24+
'base_url = "https://local.example/v1"',
25+
'',
26+
].join('\n');
27+
28+
const merged = mergeCodexConfigForWesightModel(
29+
existing,
30+
'Zhipu GLM',
31+
apiConfig.baseURL,
32+
apiConfig.model,
33+
);
34+
35+
expect(merged).toContain('# user comment');
36+
expect(merged).toContain('[features]');
37+
expect(merged).toContain('web_search_request = true');
38+
expect(merged).toContain('[model_providers.local]');
39+
expect(merged).toContain('model_provider = "zhipu_glm"');
40+
expect(merged).toContain('model = "glm-5.1-highspeed"');
41+
expect(merged).toContain('[model_providers.zhipu_glm]');
42+
expect(merged).toContain('base_url = "https://api.example.com/v1"');
43+
expect(merged).not.toContain('sk-wesight-secret');
44+
});
45+
46+
test('mergeClaudeSettingsForWesightModel preserves user credentials', () => {
47+
const merged = mergeClaudeSettingsForWesightModel({
48+
env: {
49+
ANTHROPIC_API_KEY: 'sk-user-secret',
50+
FOO_TOKEN: 'keep-me',
51+
},
52+
theme: 'dark',
53+
}, apiConfig);
54+
55+
expect(merged.theme).toBe('dark');
56+
const env = merged.env as Record<string, unknown>;
57+
expect(env.ANTHROPIC_API_KEY).toBe('sk-user-secret');
58+
expect(env.FOO_TOKEN).toBe('keep-me');
59+
expect(env.ANTHROPIC_BASE_URL).toBe(apiConfig.baseURL);
60+
expect(env.ANTHROPIC_MODEL).toBe(apiConfig.model);
61+
});
62+
63+
test('mergeClaudeSettingsForWesightModel replaces old WeSight credentials with placeholder', () => {
64+
const merged = mergeClaudeSettingsForWesightModel({
65+
env: {
66+
ANTHROPIC_API_KEY: apiConfig.apiKey,
67+
ANTHROPIC_AUTH_TOKEN: apiConfig.apiKey,
68+
},
69+
}, apiConfig);
70+
71+
const env = merged.env as Record<string, unknown>;
72+
expect(env.ANTHROPIC_API_KEY).toBe('${WESIGHT_APIKEY_ACTIVE_PROVIDER}');
73+
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('${WESIGHT_APIKEY_ACTIVE_PROVIDER}');
74+
expect(JSON.stringify(merged)).not.toContain(apiConfig.apiKey);
75+
});
76+
77+
test('buildEnvForConfig injects real secrets only into process env', () => {
78+
const env = buildEnvForConfig(apiConfig);
79+
80+
expect(env.WESIGHT_APIKEY_ACTIVE_PROVIDER).toBe(apiConfig.apiKey);
81+
expect(env.ANTHROPIC_API_KEY).toBe(apiConfig.apiKey);
82+
expect(env.OPENAI_API_KEY).toBe(apiConfig.apiKey);
83+
expect(env.OPENAI_BASE_URL).toBe(apiConfig.baseURL);
84+
expect(env.OPENAI_MODEL).toBe(apiConfig.model);
85+
});

src/main/libs/externalAgentConfigSync.ts

Lines changed: 135 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ const DEFAULT_GROK_LOCAL_MODEL = DEFAULT_GROK_BUILD_MODEL;
113113
const DEFAULT_QWEN_CODE_LOCAL_MODEL = DEFAULT_QWEN_CODE_MODEL;
114114
const DEFAULT_DEEPSEEK_TUI_LOCAL_MODEL = DEFAULT_DEEPSEEK_TUI_MODEL;
115115
const CC_SWITCH_CLAUDE_COMMON_CONFIG_KEY = 'common_config_claude';
116+
const WESIGHT_MANAGED_META_KEY = '__wesight_managed';
117+
const WESIGHT_ACTIVE_API_KEY_ENV = 'WESIGHT_APIKEY_ACTIVE_PROVIDER';
118+
const WESIGHT_ACTIVE_API_KEY_PLACEHOLDER = `\${${WESIGHT_ACTIVE_API_KEY_ENV}}`;
116119
const CLAUDE_MODEL_ENV_KEYS = [
117120
'ANTHROPIC_AUTH_TOKEN',
118121
'ANTHROPIC_API_KEY',
@@ -217,7 +220,7 @@ const sanitizeProviderKey = (value: string): string => {
217220
return key || 'wesight';
218221
};
219222

220-
const buildCodexConfig = (providerName: string, baseUrl: string, model: string): string => {
223+
export const buildCodexConfig = (providerName: string, baseUrl: string, model: string): string => {
221224
const providerKey = sanitizeProviderKey(providerName);
222225
return [
223226
`model_provider = ${tomlString(providerKey)}`,
@@ -234,6 +237,89 @@ const buildCodexConfig = (providerName: string, baseUrl: string, model: string):
234237
].filter((line) => line !== '').join('\n');
235238
};
236239

240+
const isWesightPlaceholder = (value: unknown): boolean => {
241+
return typeof value === 'string'
242+
&& /^\$\{(?:WESIGHT|LOBSTER)_[A-Z0-9_]+\}$/.test(value.trim());
243+
};
244+
245+
const removeTrailingBlankLines = (value: string): string => {
246+
return value.replace(/\s+$/g, '');
247+
};
248+
249+
const splitTomlHeadAndTables = (configText: string): { head: string; tables: string } => {
250+
const match = configText.match(/^\s*\[/m);
251+
if (!match || match.index === undefined) {
252+
return { head: configText, tables: '' };
253+
}
254+
return {
255+
head: configText.slice(0, match.index),
256+
tables: configText.slice(match.index),
257+
};
258+
};
259+
260+
const upsertTomlTopLevelString = (head: string, key: string, value: string): string => {
261+
const line = `${key} = ${tomlString(value)}`;
262+
const pattern = new RegExp(`^\\s*${key}\\s*=.*$`, 'm');
263+
if (pattern.test(head)) {
264+
return head.replace(pattern, line);
265+
}
266+
const trimmed = removeTrailingBlankLines(head);
267+
return trimmed ? `${trimmed}\n${line}\n` : `${line}\n`;
268+
};
269+
270+
const upsertTomlTopLevelBoolean = (head: string, key: string, value: boolean): string => {
271+
const line = `${key} = ${value ? 'true' : 'false'}`;
272+
const pattern = new RegExp(`^\\s*${key}\\s*=.*$`, 'm');
273+
if (pattern.test(head)) {
274+
return head.replace(pattern, line);
275+
}
276+
const trimmed = removeTrailingBlankLines(head);
277+
return trimmed ? `${trimmed}\n${line}\n` : `${line}\n`;
278+
};
279+
280+
const replaceCodexProviderTable = (
281+
tables: string,
282+
providerKey: string,
283+
providerName: string,
284+
baseUrl: string,
285+
): string => {
286+
const escaped = providerKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
287+
const providerBlock = [
288+
`[model_providers.${providerKey}]`,
289+
`name = ${tomlString(providerName || providerKey)}`,
290+
baseUrl.trim() ? `base_url = ${tomlString(baseUrl.trim())}` : '',
291+
'wire_api = "responses"',
292+
'requires_openai_auth = true',
293+
'',
294+
].filter((line) => line !== '').join('\n');
295+
const tablePattern = new RegExp(
296+
`(^|\\n)\\[model_providers\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`,
297+
'm',
298+
);
299+
if (tablePattern.test(tables)) {
300+
return tables.replace(tablePattern, (match, prefix) => `${prefix}${providerBlock}`);
301+
}
302+
const trimmed = removeTrailingBlankLines(tables);
303+
return trimmed ? `${trimmed}\n\n${providerBlock}\n` : `${providerBlock}\n`;
304+
};
305+
306+
export const mergeCodexConfigForWesightModel = (
307+
existingText: string,
308+
providerName: string,
309+
baseUrl: string,
310+
model: string,
311+
): string => {
312+
const providerKey = sanitizeProviderKey(providerName);
313+
const split = splitTomlHeadAndTables(existingText);
314+
let head = split.head;
315+
head = upsertTomlTopLevelString(head, 'model_provider', providerKey);
316+
head = upsertTomlTopLevelString(head, 'model', model || DEFAULT_CODEX_MODEL);
317+
head = upsertTomlTopLevelString(head, 'model_reasoning_effort', 'high');
318+
head = upsertTomlTopLevelBoolean(head, 'disable_response_storage', true);
319+
const tables = replaceCodexProviderTable(split.tables, providerKey, providerName, baseUrl);
320+
return `${removeTrailingBlankLines(head)}\n\n${removeTrailingBlankLines(tables)}\n`;
321+
};
322+
237323
const extractTomlString = (configText: string, key: string): string => {
238324
const match = configText.match(new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']*)["']`, 'm'));
239325
return match?.[1]?.trim() ?? '';
@@ -316,10 +402,26 @@ const buildClaudeEnvForConfig = (
316402
existingEnv: Record<string, unknown>,
317403
config: CoworkApiConfig,
318404
): Record<string, unknown> => {
405+
const existingAuthToken = getString(existingEnv.ANTHROPIC_AUTH_TOKEN);
406+
const existingApiKey = getString(existingEnv.ANTHROPIC_API_KEY);
407+
const hasUserCredential = Boolean(
408+
existingAuthToken
409+
|| existingApiKey,
410+
);
411+
const credentialMatchesWesightConfig = Boolean(
412+
config.apiKey
413+
&& (existingAuthToken === config.apiKey || existingApiKey === config.apiKey),
414+
);
415+
const shouldWriteCredential = !hasUserCredential
416+
|| credentialMatchesWesightConfig
417+
|| isWesightPlaceholder(existingEnv.ANTHROPIC_AUTH_TOKEN)
418+
|| isWesightPlaceholder(existingEnv.ANTHROPIC_API_KEY);
319419
return {
320420
...existingEnv,
321-
ANTHROPIC_AUTH_TOKEN: config.apiKey,
322-
ANTHROPIC_API_KEY: config.apiKey,
421+
...(shouldWriteCredential ? {
422+
ANTHROPIC_AUTH_TOKEN: WESIGHT_ACTIVE_API_KEY_PLACEHOLDER,
423+
ANTHROPIC_API_KEY: WESIGHT_ACTIVE_API_KEY_PLACEHOLDER,
424+
} : {}),
323425
ANTHROPIC_BASE_URL: config.baseURL,
324426
ANTHROPIC_MODEL: config.model,
325427
ANTHROPIC_REASONING_MODEL: config.model,
@@ -344,13 +446,6 @@ const mergeClaudeSettingsWithProvider = (
344446
...providerEnv,
345447
};
346448

347-
for (const key of CLAUDE_MODEL_ENV_KEYS) {
348-
if (!Object.prototype.hasOwnProperty.call(commonEnv, key)
349-
&& !Object.prototype.hasOwnProperty.call(providerEnv, key)) {
350-
delete env[key];
351-
}
352-
}
353-
354449
return {
355450
...existingSettings,
356451
...commonConfig,
@@ -359,13 +454,32 @@ const mergeClaudeSettingsWithProvider = (
359454
};
360455
};
361456

362-
const buildClaudeSettingsForConfig = (
457+
export const mergeClaudeSettingsForWesightModel = (
363458
existingSettings: Record<string, unknown>,
364459
config: CoworkApiConfig,
365460
): Record<string, unknown> => {
461+
const existingManaged = getNestedRecord(existingSettings, WESIGHT_MANAGED_META_KEY);
462+
const existingClaude = getNestedRecord(existingManaged, 'claudeCode');
463+
const previousEnvKeys = Array.isArray(existingClaude.envKeys)
464+
? existingClaude.envKeys.filter((key): key is string => typeof key === 'string')
465+
: [];
466+
const existingEnv = { ...getNestedRecord(existingSettings, 'env') };
467+
for (const key of previousEnvKeys) {
468+
if (isWesightPlaceholder(existingEnv[key])) {
469+
delete existingEnv[key];
470+
}
471+
}
472+
const env = buildClaudeEnvForConfig(existingEnv, config);
366473
return {
367474
...existingSettings,
368-
env: buildClaudeEnvForConfig(getNestedRecord(existingSettings, 'env'), config),
475+
env,
476+
[WESIGHT_MANAGED_META_KEY]: {
477+
...existingManaged,
478+
claudeCode: {
479+
envKeys: CLAUDE_MODEL_ENV_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(env, key)),
480+
secretEnv: WESIGHT_ACTIVE_API_KEY_ENV,
481+
},
482+
},
369483
};
370484
};
371485

@@ -537,7 +651,7 @@ const upsertCcSwitchClaudeProvider = (
537651
const providers = readCcSwitchClaudeProviders(db);
538652
const targetProvider = findCcSwitchProviderForConfig(providers, config, settingsCurrentProviderId, providerName);
539653
const now = Date.now();
540-
const settingsConfig = buildClaudeSettingsForConfig(targetProvider?.settingsConfig ?? {}, config);
654+
const settingsConfig = mergeClaudeSettingsForWesightModel(targetProvider?.settingsConfig ?? {}, config);
541655
const providerId = targetProvider?.id ?? randomUUID();
542656
const existingMeta = targetProvider?.meta ?? {};
543657
const meta = {
@@ -654,7 +768,7 @@ const syncClaudeCodeFromWesightModel = (): void => {
654768
}
655769

656770
const settings = readJsonObject(paths.primaryConfigPath) ?? {};
657-
writeJsonObject(paths.primaryConfigPath, buildClaudeSettingsForConfig(settings, config));
771+
writeJsonObject(paths.primaryConfigPath, mergeClaudeSettingsForWesightModel(settings, config));
658772
};
659773

660774
const syncCodexFromWesightModel = (): void => {
@@ -666,14 +780,14 @@ const syncCodexFromWesightModel = (): void => {
666780

667781
const providerName = resolved.providerMetadata?.providerName || 'wesight';
668782
const paths = getCliConfigPaths('codex');
669-
const authPath = paths.secondaryConfigPaths[0] || path.join(path.dirname(paths.primaryConfigPath), 'auth.json');
670-
const auth = readJsonObject(authPath) ?? {};
783+
const existingConfigText = fs.existsSync(paths.primaryConfigPath)
784+
? fs.readFileSync(paths.primaryConfigPath, 'utf8')
785+
: '';
671786

672-
atomicWrite(paths.primaryConfigPath, buildCodexConfig(providerName, config.baseURL, config.model));
673-
writeJsonObject(authPath, {
674-
...auth,
675-
OPENAI_API_KEY: config.apiKey,
676-
});
787+
atomicWrite(
788+
paths.primaryConfigPath,
789+
mergeCodexConfigForWesightModel(existingConfigText, providerName, config.baseURL, config.model),
790+
);
677791
};
678792

679793
export const syncOpenCodeGlobalConfigFromWesightModel = (): void => {

0 commit comments

Comments
 (0)