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:
1 parent
e3551f0e3c
commit
bf6177e15a
4 files changed
+88
-6
No files matched your search
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user