feat(codex): block OAuth launches without auth.json credentials
This commit is contained in:
1 parent
2de64a4c78
commit
66f9ce71e6
5 files changed
+106
-4
No files matched your search
@@ -7,3 +7,4 @@ cpa-core/
|
||||
|
||||
*.log
|
||||
*.tmp
|
||||
misc/
|
||||
+73
-3
@@ -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}-{}-{}",
|
||||
|
||||
@@ -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': '設定を消去',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user