fix(codex): preserve OAuth configuration across updates

- preserve the active Codex OAuth mode in legacy configuration paths
- retain unsaved OAuth selection when navigating between clients
- add regression coverage to prevent OAuth credentials from being overwritten
This commit is contained in:
lzt404 committed 2026-09-04 01:50:46 +08:00
1 parent e3551f0e3c
commit bf6177e15a
4 files changed
+88 -6

No files matched your search

+12 -2
View File
@@ -1035,6 +1035,11 @@ pub(crate) async fn set_agent_config_enabled(
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏".to_string())?;
let oauth_configuration = if client == AgentClient::Codex {
current_codex_oauth_configuration(&home)?
} else {
false
};
apply_agent_configuration_with_oauth(
client,
&home,
@@ -1044,7 +1049,7 @@ pub(crate) async fn set_agent_config_enabled(
AgentConfigurationOptions {
models: &prepared.models,
codex_catalog: prepared.codex_catalog.as_deref(),
oauth_configuration: false,
oauth_configuration,
claude_code_model_mappings: claude_code_model_mappings.as_ref(),
claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(),
},
@@ -1095,6 +1100,11 @@ pub(crate) async fn update_agent_config(
let _guard = AGENT_CONFIG_FILE_LOCK
.lock()
.map_err(|_| "智能体配置文件锁已损坏".to_string())?;
let oauth_configuration = if client == AgentClient::Codex {
current_codex_oauth_configuration(&home)?
} else {
false
};
apply_agent_configuration_with_oauth(
client,
&home,
@@ -1104,7 +1114,7 @@ pub(crate) async fn update_agent_config(
AgentConfigurationOptions {
models: &prepared.models,
codex_catalog: prepared.codex_catalog.as_deref(),
oauth_configuration: false,
oauth_configuration,
claude_code_model_mappings: claude_code_model_mappings.as_ref(),
claude_desktop_model_mappings: claude_desktop_model_mappings.as_ref(),
},
+19
View File
@@ -700,6 +700,25 @@ pub(crate) fn codex_configuration_directory(home: &Path) -> PathBuf {
.unwrap_or_else(|| home.join(".codex"))
}
pub(crate) fn current_codex_oauth_configuration(home: &Path) -> Result<bool, String> {
let path = codex_configuration_directory(home).join("config.toml");
if !path.is_file() {
return Ok(false);
}
let root: toml::Value = toml::from_str(
&fs::read_to_string(&path).map_err(|error| format!("读取 Codex 配置失败: {error}"))?,
)
.map_err(|error| format!("解析 Codex 配置失败: {error}"))?;
Ok(root
.get("model_providers")
.and_then(toml::Value::as_table)
.and_then(|providers| providers.get(MANAGED_AGENT_PROVIDER_ID))
.and_then(toml::Value::as_table)
.and_then(|provider| provider.get("requires_openai_auth"))
.and_then(toml::Value::as_bool)
== Some(true))
}
pub(crate) fn codex_auth_file_has_oauth_tokens(path: &Path) -> bool {
let Ok(content) = fs::read_to_string(path) else {
return false;
@@ -345,6 +345,52 @@ fn codex_oauth_configuration_uses_openai_auth_with_bearer_token() {
fs::remove_dir_all(home).unwrap();
}
#[test]
fn codex_oauth_configuration_is_preserved_for_legacy_updates() {
let home = agent_test_home("codex-oauth-preserved-update");
let codex_dir = home.join(".codex");
fs::create_dir_all(&codex_dir).unwrap();
fs::write(
codex_dir.join("config.toml"),
"[model_providers.cpa-gui]\nrequires_openai_auth = true\n",
)
.unwrap();
fs::write(
codex_dir.join("auth.json"),
r#"{"tokens":{"access_token":"oauth-access-token"}}"#,
)
.unwrap();
let oauth_configuration = current_codex_oauth_configuration(&home).unwrap();
assert!(oauth_configuration);
let models = test_agent_models(&["gpt-test"]);
let catalog = test_codex_models(&["gpt-test"]);
let updates = build_agent_updates_with_oauth(
AgentClient::Codex,
&home,
8317,
DEFAULT_API_KEY,
"gpt-test",
AgentConfigurationOptions {
models: &models,
codex_catalog: Some(&catalog),
oauth_configuration,
claude_code_model_mappings: None,
claude_desktop_model_mappings: None,
},
)
.unwrap();
assert!(updates[0].after.contains("requires_openai_auth = true"));
assert!(
serde_json::from_str::<serde_json::Value>(&updates[2].after).unwrap()["tokens"]
["access_token"]
.is_string()
);
fs::remove_dir_all(home).unwrap();
}
#[test]
fn codex_api_and_oauth_modes_write_the_same_catalog() {
let home = agent_test_home("codex-auth-mode-catalog");
+11 -4
View File
@@ -169,6 +169,7 @@ const createClaudeBooleanByClient = (): Record<ClaudeModelMappingClientId, boole
let claudeModelMappingsDraftCache = createClaudeModelMappingsByClient();
let claudeCustomMappingCache = createClaudeBooleanByClient();
const claudeModelMappingsDirtyCache = createClaudeBooleanByClient();
let codexOauthConfigurationDraftCache: boolean | null = null;
const claudeMappingRoles = [
{
@@ -676,7 +677,9 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
readAgentLaunchDirectoryHistory,
);
const [oauthLoginRequiredAction, setOauthLoginRequiredAction] = useState<OAuthLoginRequiredAction | null>(null);
const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState<boolean | null>(null);
const [oauthConfigurationDraft, setOauthConfigurationDraftState] = useState<boolean | null>(
() => codexOauthConfigurationDraftCache,
);
const [piProviderUpdateStatus, setPiProviderUpdateStatus] = useState<PiProviderUpdateStatus | null>(null);
const modelRequestRef = useRef(0);
const piUpdateRequestRef = useRef(0);
@@ -706,6 +709,11 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
});
}, []);
const setOauthConfigurationDraft = useCallback((value: boolean | null) => {
codexOauthConfigurationDraftCache = value;
setOauthConfigurationDraftState(value);
}, []);
const loadStatuses = useCallback(async (forceRefresh = false) => {
const command = forceRefresh
? 'refresh_agent_config_statuses'
@@ -803,9 +811,8 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
setLaunchDirectoryTarget(null);
setLaunchDirectoryError('');
setOauthLoginRequiredAction(null);
setOauthConfigurationDraft(null);
// Preserve each Claude client's unsaved draft while navigating between clients.
// Its dirty flag is cleared only after a successful apply, close, or reset action.
// Preserve unsaved client-specific configuration while navigating between clients.
// Each configuration action decides whether its draft should be retained or cleared.
}, [selected]);
const activeDefinition = agentDefinitions.find((agent) => agent.id === selected)