feat(codex): block OAuth launches without auth.json credentials

This commit is contained in:
lzt404 committed 2026-07-31 16:10:34 +08:00
1 parent 2de64a4c78
commit 66f9ce71e6
5 files changed
+106 -4

No files matched your search

+1
View File
@@ -7,3 +7,4 @@ cpa-core/
*.log
*.tmp
misc/
+73 -3
View File
@@ -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<PathBuf> {
}]
}
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::<serde_json::Value>(&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<bool, String> {
match fs::remove_file(path) {
Ok(()) => Ok(true),
@@ -2423,7 +2462,7 @@ fn remove_codex_config_file(path: &Path) -> Result<bool, String> {
}
fn clear_codex_config_files(home: &Path) -> Result<Vec<String>, 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<Vec<String>, 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<PathBuf> {
@@ -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}-{}-{}",
+3
View File
@@ -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': '設定を消去',
+6
View File
@@ -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<MessageKey, string> = {
'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',
+23 -1
View File
@@ -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<boolean | null>(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() {
</section>
</div>
) : null}
{oauthLoginRequiredOpen ? (
<div className="config-dialog-backdrop">
<section className="config-dialog agent-restore-dialog" role="alertdialog" aria-modal="true" aria-labelledby="agent-oauth-login-required-title">
<div className="config-dialog-heading">
<div><AlertTriangle size={19} /><h2 id="agent-oauth-login-required-title">{t('agents.oauthLoginRequired.title')}</h2></div>
</div>
<p>{t('agents.oauthLoginRequired.description')}</p>
<div className="config-dialog-actions">
<button type="button" className="primary-button" onClick={() => setOauthLoginRequiredOpen(false)}>{t('agents.oauthLoginRequired.confirm')}</button>
</div>
</section>
</div>
) : null}
</section>
);
}