From 66f9ce71e6a813c4fa4d96e52d20052126814d02 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Fri, 31 Jul 2026 16:10:34 +0800 Subject: [PATCH] feat(codex): block OAuth launches without auth.json credentials --- .gitignore | 1 + src-tauri/src/main.rs | 76 ++++++++++++++++++++++++++++++++++++++-- src/i18n/ja.ts | 3 ++ src/i18n/resources.ts | 6 ++++ src/pages/AgentsPage.tsx | 24 ++++++++++++- 5 files changed, 106 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 394af76..e425e99 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ cpa-core/ *.log *.tmp +misc/ diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d421762..5bb7a18 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -83,6 +83,7 @@ const DEFAULT_API_KEY_INITIAL_REMARK: &str = "默认密钥"; const DEFAULT_MANAGEMENT_SECRET_KEY: &str = "123456"; const MANAGED_AGENT_PROVIDER_ID: &str = "cpa-gui"; const CODEX_MODEL_CATALOG_FILE: &str = "cpa-gui-model-catalog.json"; +const CODEX_OAUTH_LOGIN_REQUIRED_ERROR: &str = "CODEX_OAUTH_LOGIN_REQUIRED"; const CLAUDE_DESKTOP_PROFILE_ID: &str = "00000000-0000-4000-8000-000000831700"; const LEGACY_AGENT_MODIFICATION_STATE_VERSION: u8 = 1; const AGENT_MODIFICATION_STATE_VERSION: u8 = 2; @@ -2091,6 +2092,9 @@ async fn launch_agent( status.modification_enabled, &status.modification_state, )?; + if client == AgentClient::Codex && status.oauth_configuration { + validate_codex_oauth_login(&home)?; + } if client == AgentClient::Codex && config.codex_session_repair_on_launch && status.configured { let repair_home = home.clone(); tauri::async_runtime::spawn_blocking(move || { @@ -2404,13 +2408,48 @@ fn agent_config_paths(client: AgentClient, home: &Path) -> Vec { }] } AgentClient::ClaudeDesktop => claude_desktop_config_paths(home), - AgentClient::Codex => vec![home.join(".codex/config.toml")], + AgentClient::Codex => vec![codex_configuration_directory(home).join("config.toml")], AgentClient::OpenCode => vec![home.join(".config/opencode/opencode.json")], AgentClient::OpenClaw => vec![home.join(".openclaw/openclaw.json")], AgentClient::Hermes => vec![hermes_agent_config_path(home)], } } +fn codex_configuration_directory(home: &Path) -> PathBuf { + env::var_os("CODEX_HOME") + .map(PathBuf::from) + .filter(|path| !path.as_os_str().is_empty()) + .unwrap_or_else(|| home.join(".codex")) +} + +fn codex_auth_file_has_oauth_tokens(path: &Path) -> bool { + let Ok(content) = fs::read_to_string(path) else { + return false; + }; + let Ok(root) = serde_json::from_str::(&content) else { + return false; + }; + root.get("tokens") + .and_then(serde_json::Value::as_object) + .is_some_and(|tokens| { + tokens + .values() + .any(|value| value.as_str().is_some_and(|token| !token.trim().is_empty())) + }) +} + +fn validate_codex_oauth_login(home: &Path) -> Result<(), String> { + validate_codex_oauth_login_at(&codex_configuration_directory(home).join("auth.json")) +} + +fn validate_codex_oauth_login_at(auth_path: &Path) -> Result<(), String> { + if codex_auth_file_has_oauth_tokens(auth_path) { + Ok(()) + } else { + Err(CODEX_OAUTH_LOGIN_REQUIRED_ERROR.to_string()) + } +} + fn remove_codex_config_file(path: &Path) -> Result { match fs::remove_file(path) { Ok(()) => Ok(true), @@ -2423,7 +2462,7 @@ fn remove_codex_config_file(path: &Path) -> Result { } fn clear_codex_config_files(home: &Path) -> Result, String> { - let codex_dir = home.join(".codex"); + let codex_dir = codex_configuration_directory(home); let config_path = codex_dir.join("config.toml"); let targets = [codex_dir.join("auth.json"), config_path.clone()]; let mut deleted = Vec::new(); @@ -2443,7 +2482,7 @@ fn clear_codex_config_files(home: &Path) -> Result, String> { } fn codex_model_catalog_path(home: &Path) -> PathBuf { - home.join(".codex").join(CODEX_MODEL_CATALOG_FILE) + codex_configuration_directory(home).join(CODEX_MODEL_CATALOG_FILE) } fn expected_agent_record_paths(client: AgentClient, paths: &[PathBuf]) -> Vec { @@ -13590,6 +13629,37 @@ mod tests { assert_eq!(effective_agent_api_key(&config), DEFAULT_API_KEY); } + #[test] + fn codex_oauth_launch_requires_auth_json_with_tokens() { + let home = agent_test_home("codex-oauth-login"); + let auth_path = home.join("auth.json"); + + assert_eq!( + validate_codex_oauth_login_at(&auth_path), + Err(CODEX_OAUTH_LOGIN_REQUIRED_ERROR.to_string()) + ); + + fs::write(&auth_path, "{ invalid json").unwrap(); + assert_eq!( + validate_codex_oauth_login_at(&auth_path), + Err(CODEX_OAUTH_LOGIN_REQUIRED_ERROR.to_string()) + ); + + fs::write(&auth_path, r#"{"OPENAI_API_KEY":"sk-api-key"}"#).unwrap(); + assert_eq!( + validate_codex_oauth_login_at(&auth_path), + Err(CODEX_OAUTH_LOGIN_REQUIRED_ERROR.to_string()) + ); + + fs::write( + &auth_path, + r#"{"tokens":{"access_token":"oauth-access-token"}}"#, + ) + .unwrap(); + assert!(validate_codex_oauth_login_at(&auth_path).is_ok()); + fs::remove_dir_all(home).unwrap(); + } + fn agent_test_home(name: &str) -> PathBuf { let path = std::env::temp_dir().join(format!( "cpa-gui-agent-{name}-{}-{}", diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 5863d85..f9fabbf 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -512,6 +512,9 @@ export const jaOverrides = { 'agents.model.firstSelection': '利用可能なモデル {count} 件。初回は先頭を選択します', 'agents.modify.title': '設定変更', 'agents.modify.oauthConfiguration': 'OAuth 設定方式', + 'agents.oauthLoginRequired.title': 'Codex へのログインが必要です', + 'agents.oauthLoginRequired.description': 'OAuth 設定方式が有効ですが、有効な Codex のログイン認証情報が見つかりません。起動する前に Codex で ChatGPT アカウントにログインしてください。', + 'agents.oauthLoginRequired.confirm': '了解しました', 'agents.modify.apply': '設定変更を適用', 'agents.modify.default': 'デフォルト設定', 'agents.modify.clear': '設定を消去', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 40cd90c..8da4214 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -515,6 +515,9 @@ export const zhCN = { 'agents.model.firstSelection': '{count} 个可用模型,首次默认选择第一项', 'agents.modify.title': '配置修改', 'agents.modify.oauthConfiguration': 'Oauth配置方式', + 'agents.oauthLoginRequired.title': '需要登录 Codex', + 'agents.oauthLoginRequired.description': '已开启 OAuth 配置模式,但未检测到有效的 Codex 登录凭据。请先在 Codex 中登录 ChatGPT 账号,然后再启动。', + 'agents.oauthLoginRequired.confirm': '我知道了', 'agents.modify.apply': '应用配置修改', 'agents.modify.default': '默认配置', 'agents.modify.clear': '清空配置', @@ -1357,6 +1360,9 @@ export const en: Record = { 'agents.model.firstSelection': '{count} models available; the first is selected by default', 'agents.modify.title': 'Configuration Changes', 'agents.modify.oauthConfiguration': 'OAuth configuration mode', + 'agents.oauthLoginRequired.title': 'Codex sign-in required', + 'agents.oauthLoginRequired.description': 'OAuth configuration mode is enabled, but no valid Codex sign-in credentials were found. Sign in to your ChatGPT account in Codex before launching it.', + 'agents.oauthLoginRequired.confirm': 'Got it', 'agents.modify.apply': 'Apply Configuration Changes', 'agents.modify.default': 'Default Configuration', 'agents.modify.clear': 'Clear Configuration', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index 504f004..f9b846f 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -92,6 +92,8 @@ type ChatGptCloseResult = { closedProcesses: number; }; +const CODEX_OAUTH_LOGIN_REQUIRED_ERROR = 'CODEX_OAUTH_LOGIN_REQUIRED'; + type AgentDefinition = { id: AgentClientId; name: string; @@ -502,6 +504,7 @@ export function AgentsPage() { const [closeAppError, setCloseAppError] = useState(''); const [closeAppNotice, setCloseAppNotice] = useState(''); const [closeAppConfirmOpen, setCloseAppConfirmOpen] = useState(false); + const [oauthLoginRequiredOpen, setOauthLoginRequiredOpen] = useState(false); const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState(null); const modelRequestRef = useRef(0); @@ -599,6 +602,7 @@ export function AgentsPage() { setCloseAppError(''); setCloseAppNotice(''); setCloseAppConfirmOpen(false); + setOauthLoginRequiredOpen(false); }, [selected]); const activeDefinition = agentDefinitions.find((agent) => agent.id === selected) @@ -763,7 +767,11 @@ export function AgentsPage() { } await invoke('launch_agent', { client: selected, target: target.id }); } catch (requestError) { - setLaunchError(String(requestError)); + if (String(requestError) === CODEX_OAUTH_LOGIN_REQUIRED_ERROR) { + setOauthLoginRequiredOpen(true); + } else { + setLaunchError(String(requestError)); + } } finally { setBusyAction(null); } @@ -1173,6 +1181,20 @@ export function AgentsPage() { ) : null} + + {oauthLoginRequiredOpen ? ( +
+
+
+

{t('agents.oauthLoginRequired.title')}

+
+

{t('agents.oauthLoginRequired.description')}

+
+ +
+
+
+ ) : null} ); }