Move CodexAuth and AuthManager to the core crate (#3074)
Fix a long standing layering issue.
This commit is contained in:
4
codex-rs/Cargo.lock
generated
4
codex-rs/Cargo.lock
generated
@@ -665,7 +665,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"codex-common",
|
"codex-common",
|
||||||
"codex-core",
|
"codex-core",
|
||||||
"codex-login",
|
"codex-protocol",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -718,7 +718,6 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"codex-apply-patch",
|
"codex-apply-patch",
|
||||||
"codex-login",
|
|
||||||
"codex-mcp-client",
|
"codex-mcp-client",
|
||||||
"codex-protocol",
|
"codex-protocol",
|
||||||
"core_test_support",
|
"core_test_support",
|
||||||
@@ -847,6 +846,7 @@ version = "0.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"codex-core",
|
||||||
"codex-protocol",
|
"codex-protocol",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ anyhow = "1"
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
codex-common = { path = "../common", features = ["cli"] }
|
codex-common = { path = "../common", features = ["cli"] }
|
||||||
codex-core = { path = "../core" }
|
codex-core = { path = "../core" }
|
||||||
codex-login = { path = "../login" }
|
codex-protocol = { path = "../protocol" }
|
||||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use codex_login::AuthMode;
|
use codex_core::CodexAuth;
|
||||||
use codex_login::CodexAuth;
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use codex_login::TokenData;
|
use codex_core::token_data::TokenData;
|
||||||
|
|
||||||
static CHATGPT_TOKEN: LazyLock<RwLock<Option<TokenData>>> = LazyLock::new(|| RwLock::new(None));
|
static CHATGPT_TOKEN: LazyLock<RwLock<Option<TokenData>>> = LazyLock::new(|| RwLock::new(None));
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use codex_common::CliConfigOverrides;
|
use codex_common::CliConfigOverrides;
|
||||||
|
use codex_core::CodexAuth;
|
||||||
|
use codex_core::auth::CLIENT_ID;
|
||||||
|
use codex_core::auth::OPENAI_API_KEY_ENV_VAR;
|
||||||
|
use codex_core::auth::login_with_api_key;
|
||||||
|
use codex_core::auth::logout;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::config::ConfigOverrides;
|
use codex_core::config::ConfigOverrides;
|
||||||
use codex_login::AuthMode;
|
|
||||||
use codex_login::CLIENT_ID;
|
|
||||||
use codex_login::CodexAuth;
|
|
||||||
use codex_login::OPENAI_API_KEY_ENV_VAR;
|
|
||||||
use codex_login::ServerOptions;
|
use codex_login::ServerOptions;
|
||||||
use codex_login::login_with_api_key;
|
|
||||||
use codex_login::logout;
|
|
||||||
use codex_login::run_login_server;
|
use codex_login::run_login_server;
|
||||||
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::io::IsTerminal;
|
|||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use codex_common::CliConfigOverrides;
|
use codex_common::CliConfigOverrides;
|
||||||
|
use codex_core::AuthManager;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::NewConversation;
|
use codex_core::NewConversation;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
@@ -9,7 +10,6 @@ use codex_core::config::ConfigOverrides;
|
|||||||
use codex_core::protocol::Event;
|
use codex_core::protocol::Event;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
use codex_core::protocol::Submission;
|
use codex_core::protocol::Submission;
|
||||||
use codex_login::AuthManager;
|
|
||||||
use tokio::io::AsyncBufReadExt;
|
use tokio::io::AsyncBufReadExt;
|
||||||
use tokio::io::BufReader;
|
use tokio::io::BufReader;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ base64 = "0.22"
|
|||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
codex-apply-patch = { path = "../apply-patch" }
|
codex-apply-patch = { path = "../apply-patch" }
|
||||||
codex-login = { path = "../login" }
|
|
||||||
codex-mcp-client = { path = "../mcp-client" }
|
codex-mcp-client = { path = "../mcp-client" }
|
||||||
codex-protocol = { path = "../protocol" }
|
codex-protocol = { path = "../protocol" }
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
|
|||||||
766
codex-rs/core/src/auth.rs
Normal file
766
codex-rs/core/src/auth.rs
Normal file
@@ -0,0 +1,766 @@
|
|||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::io::Write;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
|
|
||||||
|
use crate::token_data::TokenData;
|
||||||
|
use crate::token_data::parse_id_token;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CodexAuth {
|
||||||
|
pub mode: AuthMode,
|
||||||
|
|
||||||
|
pub(crate) api_key: Option<String>,
|
||||||
|
pub(crate) auth_dot_json: Arc<Mutex<Option<AuthDotJson>>>,
|
||||||
|
pub(crate) auth_file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for CodexAuth {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.mode == other.mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodexAuth {
|
||||||
|
pub fn from_api_key(api_key: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
api_key: Some(api_key.to_owned()),
|
||||||
|
mode: AuthMode::ApiKey,
|
||||||
|
auth_file: PathBuf::new(),
|
||||||
|
auth_dot_json: Arc::new(Mutex::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_token(&self) -> Result<String, std::io::Error> {
|
||||||
|
let token_data = self
|
||||||
|
.get_current_token_data()
|
||||||
|
.ok_or(std::io::Error::other("Token data is not available."))?;
|
||||||
|
let token = token_data.refresh_token;
|
||||||
|
|
||||||
|
let refresh_response = try_refresh_token(token)
|
||||||
|
.await
|
||||||
|
.map_err(std::io::Error::other)?;
|
||||||
|
|
||||||
|
let updated = update_tokens(
|
||||||
|
&self.auth_file,
|
||||||
|
refresh_response.id_token,
|
||||||
|
refresh_response.access_token,
|
||||||
|
refresh_response.refresh_token,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Ok(mut auth_lock) = self.auth_dot_json.lock() {
|
||||||
|
*auth_lock = Some(updated.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let access = match updated.tokens {
|
||||||
|
Some(t) => t.access_token,
|
||||||
|
None => {
|
||||||
|
return Err(std::io::Error::other(
|
||||||
|
"Token data is not available after refresh.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(access)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the available auth information from the auth.json or
|
||||||
|
/// OPENAI_API_KEY environment variable.
|
||||||
|
pub fn from_codex_home(
|
||||||
|
codex_home: &Path,
|
||||||
|
preferred_auth_method: AuthMode,
|
||||||
|
) -> std::io::Result<Option<CodexAuth>> {
|
||||||
|
load_auth(codex_home, true, preferred_auth_method)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_token_data(&self) -> Result<TokenData, std::io::Error> {
|
||||||
|
let auth_dot_json: Option<AuthDotJson> = self.get_current_auth_json();
|
||||||
|
match auth_dot_json {
|
||||||
|
Some(AuthDotJson {
|
||||||
|
tokens: Some(mut tokens),
|
||||||
|
last_refresh: Some(last_refresh),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if last_refresh < Utc::now() - chrono::Duration::days(28) {
|
||||||
|
let refresh_response = tokio::time::timeout(
|
||||||
|
Duration::from_secs(60),
|
||||||
|
try_refresh_token(tokens.refresh_token.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
std::io::Error::other("timed out while refreshing OpenAI API key")
|
||||||
|
})?
|
||||||
|
.map_err(std::io::Error::other)?;
|
||||||
|
|
||||||
|
let updated_auth_dot_json = update_tokens(
|
||||||
|
&self.auth_file,
|
||||||
|
refresh_response.id_token,
|
||||||
|
refresh_response.access_token,
|
||||||
|
refresh_response.refresh_token,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tokens = updated_auth_dot_json
|
||||||
|
.tokens
|
||||||
|
.clone()
|
||||||
|
.ok_or(std::io::Error::other(
|
||||||
|
"Token data is not available after refresh.",
|
||||||
|
))?;
|
||||||
|
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
let mut auth_lock = self.auth_dot_json.lock().unwrap();
|
||||||
|
*auth_lock = Some(updated_auth_dot_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tokens)
|
||||||
|
}
|
||||||
|
_ => Err(std::io::Error::other("Token data is not available.")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_token(&self) -> Result<String, std::io::Error> {
|
||||||
|
match self.mode {
|
||||||
|
AuthMode::ApiKey => Ok(self.api_key.clone().unwrap_or_default()),
|
||||||
|
AuthMode::ChatGPT => {
|
||||||
|
let id_token = self.get_token_data().await?.access_token;
|
||||||
|
Ok(id_token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_account_id(&self) -> Option<String> {
|
||||||
|
self.get_current_token_data()
|
||||||
|
.and_then(|t| t.account_id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_plan_type(&self) -> Option<String> {
|
||||||
|
self.get_current_token_data()
|
||||||
|
.and_then(|t| t.id_token.chatgpt_plan_type.as_ref().map(|p| p.as_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_auth_json(&self) -> Option<AuthDotJson> {
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
self.auth_dot_json.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_token_data(&self) -> Option<TokenData> {
|
||||||
|
self.get_current_auth_json().and_then(|t| t.tokens.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consider this private to integration tests.
|
||||||
|
pub fn create_dummy_chatgpt_auth_for_testing() -> Self {
|
||||||
|
let auth_dot_json = AuthDotJson {
|
||||||
|
openai_api_key: None,
|
||||||
|
tokens: Some(TokenData {
|
||||||
|
id_token: Default::default(),
|
||||||
|
access_token: "Access Token".to_string(),
|
||||||
|
refresh_token: "test".to_string(),
|
||||||
|
account_id: Some("account_id".to_string()),
|
||||||
|
}),
|
||||||
|
last_refresh: Some(Utc::now()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let auth_dot_json = Arc::new(Mutex::new(Some(auth_dot_json)));
|
||||||
|
Self {
|
||||||
|
api_key: None,
|
||||||
|
mode: AuthMode::ChatGPT,
|
||||||
|
auth_file: PathBuf::new(),
|
||||||
|
auth_dot_json,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
|
||||||
|
|
||||||
|
fn read_openai_api_key_from_env() -> Option<String> {
|
||||||
|
env::var(OPENAI_API_KEY_ENV_VAR)
|
||||||
|
.ok()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_auth_file(codex_home: &Path) -> PathBuf {
|
||||||
|
codex_home.join("auth.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete the auth.json file inside `codex_home` if it exists. Returns `Ok(true)`
|
||||||
|
/// if a file was removed, `Ok(false)` if no auth file was present.
|
||||||
|
pub fn logout(codex_home: &Path) -> std::io::Result<bool> {
|
||||||
|
let auth_file = get_auth_file(codex_home);
|
||||||
|
match std::fs::remove_file(&auth_file) {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes an `auth.json` that contains only the API key. Intended for CLI use.
|
||||||
|
pub fn login_with_api_key(codex_home: &Path, api_key: &str) -> std::io::Result<()> {
|
||||||
|
let auth_dot_json = AuthDotJson {
|
||||||
|
openai_api_key: Some(api_key.to_string()),
|
||||||
|
tokens: None,
|
||||||
|
last_refresh: None,
|
||||||
|
};
|
||||||
|
write_auth_json(&get_auth_file(codex_home), &auth_dot_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_auth(
|
||||||
|
codex_home: &Path,
|
||||||
|
include_env_var: bool,
|
||||||
|
preferred_auth_method: AuthMode,
|
||||||
|
) -> std::io::Result<Option<CodexAuth>> {
|
||||||
|
// First, check to see if there is a valid auth.json file. If not, we fall
|
||||||
|
// back to AuthMode::ApiKey using the OPENAI_API_KEY environment variable
|
||||||
|
// (if it is set).
|
||||||
|
let auth_file = get_auth_file(codex_home);
|
||||||
|
let auth_dot_json = match try_read_auth_json(&auth_file) {
|
||||||
|
Ok(auth) => auth,
|
||||||
|
// If auth.json does not exist, try to read the OPENAI_API_KEY from the
|
||||||
|
// environment variable.
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound && include_env_var => {
|
||||||
|
return match read_openai_api_key_from_env() {
|
||||||
|
Some(api_key) => Ok(Some(CodexAuth::from_api_key(&api_key))),
|
||||||
|
None => Ok(None),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Though if auth.json exists but is malformed, do not fall back to the
|
||||||
|
// env var because the user may be expecting to use AuthMode::ChatGPT.
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let AuthDotJson {
|
||||||
|
openai_api_key: auth_json_api_key,
|
||||||
|
tokens,
|
||||||
|
last_refresh,
|
||||||
|
} = auth_dot_json;
|
||||||
|
|
||||||
|
// If the auth.json has an API key AND does not appear to be on a plan that
|
||||||
|
// should prefer AuthMode::ChatGPT, use AuthMode::ApiKey.
|
||||||
|
if let Some(api_key) = &auth_json_api_key {
|
||||||
|
// Should any of these be AuthMode::ChatGPT with the api_key set?
|
||||||
|
// Does AuthMode::ChatGPT indicate that there is an auth.json that is
|
||||||
|
// "refreshable" even if we are using the API key for auth?
|
||||||
|
match &tokens {
|
||||||
|
Some(tokens) => {
|
||||||
|
if tokens.should_use_api_key(preferred_auth_method, tokens.is_openai_email()) {
|
||||||
|
return Ok(Some(CodexAuth::from_api_key(api_key)));
|
||||||
|
} else {
|
||||||
|
// Ignore the API key and fall through to ChatGPT auth.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// We have an API key but no tokens in the auth.json file.
|
||||||
|
// Perhaps the user ran `codex login --api-key <KEY>` or updated
|
||||||
|
// auth.json by hand. Either way, let's assume they are trying
|
||||||
|
// to use their API key.
|
||||||
|
return Ok(Some(CodexAuth::from_api_key(api_key)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the AuthMode::ChatGPT variant, perhaps neither api_key nor
|
||||||
|
// openai_api_key should exist?
|
||||||
|
Ok(Some(CodexAuth {
|
||||||
|
api_key: None,
|
||||||
|
mode: AuthMode::ChatGPT,
|
||||||
|
auth_file,
|
||||||
|
auth_dot_json: Arc::new(Mutex::new(Some(AuthDotJson {
|
||||||
|
openai_api_key: None,
|
||||||
|
tokens,
|
||||||
|
last_refresh,
|
||||||
|
}))),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to read and refresh the `auth.json` file in the given `CODEX_HOME` directory.
|
||||||
|
/// Returns the full AuthDotJson structure after refreshing if necessary.
|
||||||
|
pub fn try_read_auth_json(auth_file: &Path) -> std::io::Result<AuthDotJson> {
|
||||||
|
let mut file = File::open(auth_file)?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)?;
|
||||||
|
let auth_dot_json: AuthDotJson = serde_json::from_str(&contents)?;
|
||||||
|
|
||||||
|
Ok(auth_dot_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_auth_json(auth_file: &Path, auth_dot_json: &AuthDotJson) -> std::io::Result<()> {
|
||||||
|
let json_data = serde_json::to_string_pretty(auth_dot_json)?;
|
||||||
|
let mut options = OpenOptions::new();
|
||||||
|
options.truncate(true).write(true).create(true);
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
options.mode(0o600);
|
||||||
|
}
|
||||||
|
let mut file = options.open(auth_file)?;
|
||||||
|
file.write_all(json_data.as_bytes())?;
|
||||||
|
file.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_tokens(
|
||||||
|
auth_file: &Path,
|
||||||
|
id_token: String,
|
||||||
|
access_token: Option<String>,
|
||||||
|
refresh_token: Option<String>,
|
||||||
|
) -> std::io::Result<AuthDotJson> {
|
||||||
|
let mut auth_dot_json = try_read_auth_json(auth_file)?;
|
||||||
|
|
||||||
|
let tokens = auth_dot_json.tokens.get_or_insert_with(TokenData::default);
|
||||||
|
tokens.id_token = parse_id_token(&id_token).map_err(std::io::Error::other)?;
|
||||||
|
if let Some(access_token) = access_token {
|
||||||
|
tokens.access_token = access_token.to_string();
|
||||||
|
}
|
||||||
|
if let Some(refresh_token) = refresh_token {
|
||||||
|
tokens.refresh_token = refresh_token.to_string();
|
||||||
|
}
|
||||||
|
auth_dot_json.last_refresh = Some(Utc::now());
|
||||||
|
write_auth_json(auth_file, &auth_dot_json)?;
|
||||||
|
Ok(auth_dot_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_refresh_token(refresh_token: String) -> std::io::Result<RefreshResponse> {
|
||||||
|
let refresh_request = RefreshRequest {
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token,
|
||||||
|
scope: "openid profile email",
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.post("https://auth.openai.com/oauth/token")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&refresh_request)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(std::io::Error::other)?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
let refresh_response = response
|
||||||
|
.json::<RefreshResponse>()
|
||||||
|
.await
|
||||||
|
.map_err(std::io::Error::other)?;
|
||||||
|
Ok(refresh_response)
|
||||||
|
} else {
|
||||||
|
Err(std::io::Error::other(format!(
|
||||||
|
"Failed to refresh token: {}",
|
||||||
|
response.status()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct RefreshRequest {
|
||||||
|
client_id: &'static str,
|
||||||
|
grant_type: &'static str,
|
||||||
|
refresh_token: String,
|
||||||
|
scope: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
struct RefreshResponse {
|
||||||
|
id_token: String,
|
||||||
|
access_token: Option<String>,
|
||||||
|
refresh_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expected structure for $CODEX_HOME/auth.json.
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct AuthDotJson {
|
||||||
|
#[serde(rename = "OPENAI_API_KEY")]
|
||||||
|
pub openai_api_key: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tokens: Option<TokenData>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_refresh: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared constant for token refresh (client id used for oauth token refresh flow)
|
||||||
|
pub const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
|
||||||
|
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
/// Internal cached auth state.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct CachedAuth {
|
||||||
|
preferred_auth_mode: AuthMode,
|
||||||
|
auth: Option<CodexAuth>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::token_data::IdTokenInfo;
|
||||||
|
use crate::token_data::KnownPlan;
|
||||||
|
use crate::token_data::PlanType;
|
||||||
|
use base64::Engine;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
const LAST_REFRESH: &str = "2025-08-06T20:41:36.232376Z";
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn roundtrip_auth_dot_json() {
|
||||||
|
let codex_home = tempdir().unwrap();
|
||||||
|
let _ = write_auth_file(
|
||||||
|
AuthFileParams {
|
||||||
|
openai_api_key: None,
|
||||||
|
chatgpt_plan_type: "pro".to_string(),
|
||||||
|
},
|
||||||
|
codex_home.path(),
|
||||||
|
)
|
||||||
|
.expect("failed to write auth file");
|
||||||
|
|
||||||
|
let file = get_auth_file(codex_home.path());
|
||||||
|
let auth_dot_json = try_read_auth_json(&file).unwrap();
|
||||||
|
write_auth_json(&file, &auth_dot_json).unwrap();
|
||||||
|
|
||||||
|
let same_auth_dot_json = try_read_auth_json(&file).unwrap();
|
||||||
|
assert_eq!(auth_dot_json, same_auth_dot_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||||
|
let codex_home = tempdir().unwrap();
|
||||||
|
let fake_jwt = write_auth_file(
|
||||||
|
AuthFileParams {
|
||||||
|
openai_api_key: None,
|
||||||
|
chatgpt_plan_type: "pro".to_string(),
|
||||||
|
},
|
||||||
|
codex_home.path(),
|
||||||
|
)
|
||||||
|
.expect("failed to write auth file");
|
||||||
|
|
||||||
|
let CodexAuth {
|
||||||
|
api_key,
|
||||||
|
mode,
|
||||||
|
auth_dot_json,
|
||||||
|
auth_file: _,
|
||||||
|
} = super::load_auth(codex_home.path(), false, AuthMode::ChatGPT)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(None, api_key);
|
||||||
|
assert_eq!(AuthMode::ChatGPT, mode);
|
||||||
|
|
||||||
|
let guard = auth_dot_json.lock().unwrap();
|
||||||
|
let auth_dot_json = guard.as_ref().expect("AuthDotJson should exist");
|
||||||
|
assert_eq!(
|
||||||
|
&AuthDotJson {
|
||||||
|
openai_api_key: None,
|
||||||
|
tokens: Some(TokenData {
|
||||||
|
id_token: IdTokenInfo {
|
||||||
|
email: Some("user@example.com".to_string()),
|
||||||
|
chatgpt_plan_type: Some(PlanType::Known(KnownPlan::Pro)),
|
||||||
|
raw_jwt: fake_jwt,
|
||||||
|
},
|
||||||
|
access_token: "test-access-token".to_string(),
|
||||||
|
refresh_token: "test-refresh-token".to_string(),
|
||||||
|
account_id: None,
|
||||||
|
}),
|
||||||
|
last_refresh: Some(
|
||||||
|
DateTime::parse_from_rfc3339(LAST_REFRESH)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&Utc)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
auth_dot_json
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Even if the OPENAI_API_KEY is set in auth.json, if the plan is not in
|
||||||
|
/// [`TokenData::is_plan_that_should_use_api_key`], it should use
|
||||||
|
/// [`AuthMode::ChatGPT`].
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pro_account_with_api_key_still_uses_chatgpt_auth() {
|
||||||
|
let codex_home = tempdir().unwrap();
|
||||||
|
let fake_jwt = write_auth_file(
|
||||||
|
AuthFileParams {
|
||||||
|
openai_api_key: Some("sk-test-key".to_string()),
|
||||||
|
chatgpt_plan_type: "pro".to_string(),
|
||||||
|
},
|
||||||
|
codex_home.path(),
|
||||||
|
)
|
||||||
|
.expect("failed to write auth file");
|
||||||
|
|
||||||
|
let CodexAuth {
|
||||||
|
api_key,
|
||||||
|
mode,
|
||||||
|
auth_dot_json,
|
||||||
|
auth_file: _,
|
||||||
|
} = super::load_auth(codex_home.path(), false, AuthMode::ChatGPT)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(None, api_key);
|
||||||
|
assert_eq!(AuthMode::ChatGPT, mode);
|
||||||
|
|
||||||
|
let guard = auth_dot_json.lock().unwrap();
|
||||||
|
let auth_dot_json = guard.as_ref().expect("AuthDotJson should exist");
|
||||||
|
assert_eq!(
|
||||||
|
&AuthDotJson {
|
||||||
|
openai_api_key: None,
|
||||||
|
tokens: Some(TokenData {
|
||||||
|
id_token: IdTokenInfo {
|
||||||
|
email: Some("user@example.com".to_string()),
|
||||||
|
chatgpt_plan_type: Some(PlanType::Known(KnownPlan::Pro)),
|
||||||
|
raw_jwt: fake_jwt,
|
||||||
|
},
|
||||||
|
access_token: "test-access-token".to_string(),
|
||||||
|
refresh_token: "test-refresh-token".to_string(),
|
||||||
|
account_id: None,
|
||||||
|
}),
|
||||||
|
last_refresh: Some(
|
||||||
|
DateTime::parse_from_rfc3339(LAST_REFRESH)
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&Utc)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
auth_dot_json
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the OPENAI_API_KEY is set in auth.json and it is an enterprise
|
||||||
|
/// account, then it should use [`AuthMode::ApiKey`].
|
||||||
|
#[tokio::test]
|
||||||
|
async fn enterprise_account_with_api_key_uses_apikey_auth() {
|
||||||
|
let codex_home = tempdir().unwrap();
|
||||||
|
write_auth_file(
|
||||||
|
AuthFileParams {
|
||||||
|
openai_api_key: Some("sk-test-key".to_string()),
|
||||||
|
chatgpt_plan_type: "enterprise".to_string(),
|
||||||
|
},
|
||||||
|
codex_home.path(),
|
||||||
|
)
|
||||||
|
.expect("failed to write auth file");
|
||||||
|
|
||||||
|
let CodexAuth {
|
||||||
|
api_key,
|
||||||
|
mode,
|
||||||
|
auth_dot_json,
|
||||||
|
auth_file: _,
|
||||||
|
} = super::load_auth(codex_home.path(), false, AuthMode::ChatGPT)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(Some("sk-test-key".to_string()), api_key);
|
||||||
|
assert_eq!(AuthMode::ApiKey, mode);
|
||||||
|
|
||||||
|
let guard = auth_dot_json.lock().expect("should unwrap");
|
||||||
|
assert!(guard.is_none(), "auth_dot_json should be None");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn loads_api_key_from_auth_json() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let auth_file = dir.path().join("auth.json");
|
||||||
|
std::fs::write(
|
||||||
|
auth_file,
|
||||||
|
r#"{"OPENAI_API_KEY":"sk-test-key","tokens":null,"last_refresh":null}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let auth = super::load_auth(dir.path(), false, AuthMode::ChatGPT)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(auth.mode, AuthMode::ApiKey);
|
||||||
|
assert_eq!(auth.api_key, Some("sk-test-key".to_string()));
|
||||||
|
|
||||||
|
assert!(auth.get_token_data().await.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logout_removes_auth_file() -> Result<(), std::io::Error> {
|
||||||
|
let dir = tempdir()?;
|
||||||
|
let auth_dot_json = AuthDotJson {
|
||||||
|
openai_api_key: Some("sk-test-key".to_string()),
|
||||||
|
tokens: None,
|
||||||
|
last_refresh: None,
|
||||||
|
};
|
||||||
|
write_auth_json(&get_auth_file(dir.path()), &auth_dot_json)?;
|
||||||
|
assert!(dir.path().join("auth.json").exists());
|
||||||
|
let removed = logout(dir.path())?;
|
||||||
|
assert!(removed);
|
||||||
|
assert!(!dir.path().join("auth.json").exists());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AuthFileParams {
|
||||||
|
openai_api_key: Option<String>,
|
||||||
|
chatgpt_plan_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_auth_file(params: AuthFileParams, codex_home: &Path) -> std::io::Result<String> {
|
||||||
|
let auth_file = get_auth_file(codex_home);
|
||||||
|
// Create a minimal valid JWT for the id_token field.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Header {
|
||||||
|
alg: &'static str,
|
||||||
|
typ: &'static str,
|
||||||
|
}
|
||||||
|
let header = Header {
|
||||||
|
alg: "none",
|
||||||
|
typ: "JWT",
|
||||||
|
};
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"email": "user@example.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"https://api.openai.com/auth": {
|
||||||
|
"chatgpt_account_id": "bc3618e3-489d-4d49-9362-1561dc53ba53",
|
||||||
|
"chatgpt_plan_type": params.chatgpt_plan_type,
|
||||||
|
"chatgpt_user_id": "user-12345",
|
||||||
|
"user_id": "user-12345",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let b64 = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b);
|
||||||
|
let header_b64 = b64(&serde_json::to_vec(&header)?);
|
||||||
|
let payload_b64 = b64(&serde_json::to_vec(&payload)?);
|
||||||
|
let signature_b64 = b64(b"sig");
|
||||||
|
let fake_jwt = format!("{header_b64}.{payload_b64}.{signature_b64}");
|
||||||
|
|
||||||
|
let auth_json_data = json!({
|
||||||
|
"OPENAI_API_KEY": params.openai_api_key,
|
||||||
|
"tokens": {
|
||||||
|
"id_token": fake_jwt,
|
||||||
|
"access_token": "test-access-token",
|
||||||
|
"refresh_token": "test-refresh-token"
|
||||||
|
},
|
||||||
|
"last_refresh": LAST_REFRESH,
|
||||||
|
});
|
||||||
|
let auth_json = serde_json::to_string_pretty(&auth_json_data)?;
|
||||||
|
std::fs::write(auth_file, auth_json)?;
|
||||||
|
Ok(fake_jwt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Central manager providing a single source of truth for auth.json derived
|
||||||
|
/// authentication data. It loads once (or on preference change) and then
|
||||||
|
/// hands out cloned `CodexAuth` values so the rest of the program has a
|
||||||
|
/// consistent snapshot.
|
||||||
|
///
|
||||||
|
/// External modifications to `auth.json` will NOT be observed until
|
||||||
|
/// `reload()` is called explicitly. This matches the design goal of avoiding
|
||||||
|
/// different parts of the program seeing inconsistent auth data mid‑run.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AuthManager {
|
||||||
|
codex_home: PathBuf,
|
||||||
|
inner: RwLock<CachedAuth>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthManager {
|
||||||
|
/// Create a new manager loading the initial auth using the provided
|
||||||
|
/// preferred auth method. Errors loading auth are swallowed; `auth()` will
|
||||||
|
/// simply return `None` in that case so callers can treat it as an
|
||||||
|
/// unauthenticated state.
|
||||||
|
pub fn new(codex_home: PathBuf, preferred_auth_mode: AuthMode) -> Self {
|
||||||
|
let auth = CodexAuth::from_codex_home(&codex_home, preferred_auth_mode)
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
Self {
|
||||||
|
codex_home,
|
||||||
|
inner: RwLock::new(CachedAuth {
|
||||||
|
preferred_auth_mode,
|
||||||
|
auth,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an AuthManager with a specific CodexAuth, for testing only.
|
||||||
|
pub fn from_auth_for_testing(auth: CodexAuth) -> Arc<Self> {
|
||||||
|
let preferred_auth_mode = auth.mode;
|
||||||
|
let cached = CachedAuth {
|
||||||
|
preferred_auth_mode,
|
||||||
|
auth: Some(auth),
|
||||||
|
};
|
||||||
|
Arc::new(Self {
|
||||||
|
codex_home: PathBuf::new(),
|
||||||
|
inner: RwLock::new(cached),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current cached auth (clone). May be `None` if not logged in or load failed.
|
||||||
|
pub fn auth(&self) -> Option<CodexAuth> {
|
||||||
|
self.inner.read().ok().and_then(|c| c.auth.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preferred auth method used when (re)loading.
|
||||||
|
pub fn preferred_auth_method(&self) -> AuthMode {
|
||||||
|
self.inner
|
||||||
|
.read()
|
||||||
|
.map(|c| c.preferred_auth_mode)
|
||||||
|
.unwrap_or(AuthMode::ApiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force a reload using the existing preferred auth method. Returns
|
||||||
|
/// whether the auth value changed.
|
||||||
|
pub fn reload(&self) -> bool {
|
||||||
|
let preferred = self.preferred_auth_method();
|
||||||
|
let new_auth = CodexAuth::from_codex_home(&self.codex_home, preferred)
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
if let Ok(mut guard) = self.inner.write() {
|
||||||
|
let changed = !AuthManager::auths_equal(&guard.auth, &new_auth);
|
||||||
|
guard.auth = new_auth;
|
||||||
|
changed
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auths_equal(a: &Option<CodexAuth>, b: &Option<CodexAuth>) -> bool {
|
||||||
|
match (a, b) {
|
||||||
|
(None, None) => true,
|
||||||
|
(Some(a), Some(b)) => a == b,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience constructor returning an `Arc` wrapper.
|
||||||
|
pub fn shared(codex_home: PathBuf, preferred_auth_mode: AuthMode) -> Arc<Self> {
|
||||||
|
Arc::new(Self::new(codex_home, preferred_auth_mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to refresh the current auth token (if any). On success, reload
|
||||||
|
/// the auth state from disk so other components observe refreshed token.
|
||||||
|
pub async fn refresh_token(&self) -> std::io::Result<Option<String>> {
|
||||||
|
let auth = match self.auth() {
|
||||||
|
Some(a) => a,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
match auth.refresh_token().await {
|
||||||
|
Ok(token) => {
|
||||||
|
// Reload to pick up persisted changes.
|
||||||
|
self.reload();
|
||||||
|
Ok(Some(token))
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log out by deleting the on‑disk auth.json (if present). Returns Ok(true)
|
||||||
|
/// if a file was removed, Ok(false) if no auth file existed. On success,
|
||||||
|
/// reloads the in‑memory auth cache so callers immediately observe the
|
||||||
|
/// unauthenticated state.
|
||||||
|
pub fn logout(&self) -> std::io::Result<bool> {
|
||||||
|
let removed = super::auth::logout(&self.codex_home)?;
|
||||||
|
// Always reload to clear any cached auth (even if file absent).
|
||||||
|
self.reload();
|
||||||
|
Ok(removed)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ use std::path::Path;
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::AuthManager;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use codex_login::AuthManager;
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
use codex_login::AuthMode;
|
|
||||||
use eventsource_stream::Eventsource;
|
use eventsource_stream::Eventsource;
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use regex_lite::Regex;
|
use regex_lite::Regex;
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ use std::sync::MutexGuard;
|
|||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::AuthManager;
|
||||||
use async_channel::Receiver;
|
use async_channel::Receiver;
|
||||||
use async_channel::Sender;
|
use async_channel::Sender;
|
||||||
use codex_apply_patch::ApplyPatchAction;
|
use codex_apply_patch::ApplyPatchAction;
|
||||||
use codex_apply_patch::MaybeApplyPatchVerified;
|
use codex_apply_patch::MaybeApplyPatchVerified;
|
||||||
use codex_apply_patch::maybe_parse_apply_patch_verified;
|
use codex_apply_patch::maybe_parse_apply_patch_verified;
|
||||||
use codex_login::AuthManager;
|
|
||||||
use codex_protocol::protocol::ConversationHistoryResponseEvent;
|
use codex_protocol::protocol::ConversationHistoryResponseEvent;
|
||||||
use codex_protocol::protocol::TaskStartedEvent;
|
use codex_protocol::protocol::TaskStartedEvent;
|
||||||
use codex_protocol::protocol::TurnAbortReason;
|
use codex_protocol::protocol::TurnAbortReason;
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ use crate::model_provider_info::built_in_model_providers;
|
|||||||
use crate::openai_model_info::get_model_info;
|
use crate::openai_model_info::get_model_info;
|
||||||
use crate::protocol::AskForApproval;
|
use crate::protocol::AskForApproval;
|
||||||
use crate::protocol::SandboxPolicy;
|
use crate::protocol::SandboxPolicy;
|
||||||
use codex_login::AuthMode;
|
|
||||||
use codex_protocol::config_types::ReasoningEffort;
|
use codex_protocol::config_types::ReasoningEffort;
|
||||||
use codex_protocol::config_types::ReasoningSummary;
|
use codex_protocol::config_types::ReasoningSummary;
|
||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::collections::HashMap;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use codex_login::AuthManager;
|
use crate::AuthManager;
|
||||||
use codex_login::CodexAuth;
|
use crate::CodexAuth;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ impl ConversationManager {
|
|||||||
/// Construct with a dummy AuthManager containing the provided CodexAuth.
|
/// Construct with a dummy AuthManager containing the provided CodexAuth.
|
||||||
/// Used for integration tests: should not be used by ordinary business logic.
|
/// Used for integration tests: should not be used by ordinary business logic.
|
||||||
pub fn with_auth(auth: CodexAuth) -> Self {
|
pub fn with_auth(auth: CodexAuth) -> Self {
|
||||||
Self::new(codex_login::AuthManager::from_auth_for_testing(auth))
|
Self::new(crate::AuthManager::from_auth_for_testing(auth))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn new_conversation(&self, config: Config) -> CodexResult<NewConversation> {
|
pub async fn new_conversation(&self, config: Config) -> CodexResult<NewConversation> {
|
||||||
|
|||||||
@@ -6,12 +6,14 @@
|
|||||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||||
|
|
||||||
mod apply_patch;
|
mod apply_patch;
|
||||||
|
pub mod auth;
|
||||||
mod bash;
|
mod bash;
|
||||||
mod chat_completions;
|
mod chat_completions;
|
||||||
mod client;
|
mod client;
|
||||||
mod client_common;
|
mod client_common;
|
||||||
pub mod codex;
|
pub mod codex;
|
||||||
mod codex_conversation;
|
mod codex_conversation;
|
||||||
|
pub mod token_data;
|
||||||
pub use codex_conversation::CodexConversation;
|
pub use codex_conversation::CodexConversation;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod config_profile;
|
pub mod config_profile;
|
||||||
@@ -40,6 +42,9 @@ pub use model_provider_info::create_oss_provider_with_base_url;
|
|||||||
mod conversation_manager;
|
mod conversation_manager;
|
||||||
pub use conversation_manager::ConversationManager;
|
pub use conversation_manager::ConversationManager;
|
||||||
pub use conversation_manager::NewConversation;
|
pub use conversation_manager::NewConversation;
|
||||||
|
// Re-export common auth types for workspace consumers
|
||||||
|
pub use auth::AuthManager;
|
||||||
|
pub use auth::CodexAuth;
|
||||||
pub mod model_family;
|
pub mod model_family;
|
||||||
mod openai_model_info;
|
mod openai_model_info;
|
||||||
mod openai_tools;
|
mod openai_tools;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
//! 2. User-defined entries inside `~/.codex/config.toml` under the `model_providers`
|
//! 2. User-defined entries inside `~/.codex/config.toml` under the `model_providers`
|
||||||
//! key. These override or extend the defaults at runtime.
|
//! key. These override or extend the defaults at runtime.
|
||||||
|
|
||||||
use codex_login::AuthMode;
|
use crate::CodexAuth;
|
||||||
use codex_login::CodexAuth;
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use serde::Deserialize;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::AuthMode;
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)]
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)]
|
||||||
pub struct TokenData {
|
pub struct TokenData {
|
||||||
@@ -58,7 +58,7 @@ pub struct IdTokenInfo {
|
|||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
/// The ChatGPT subscription plan type
|
/// The ChatGPT subscription plan type
|
||||||
/// (e.g., "free", "plus", "pro", "business", "enterprise", "edu").
|
/// (e.g., "free", "plus", "pro", "business", "enterprise", "edu").
|
||||||
/// (Note: ae has not verified that those are the exact values.)
|
/// (Note: values may vary by backend.)
|
||||||
pub(crate) chatgpt_plan_type: Option<PlanType>,
|
pub(crate) chatgpt_plan_type: Option<PlanType>,
|
||||||
pub raw_jwt: String,
|
pub raw_jwt: String,
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ pub enum IdTokenInfoError {
|
|||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_id_token(id_token: &str) -> Result<IdTokenInfo, IdTokenInfoError> {
|
pub fn parse_id_token(id_token: &str) -> Result<IdTokenInfo, IdTokenInfoError> {
|
||||||
// JWT format: header.payload.signature
|
// JWT format: header.payload.signature
|
||||||
let mut parts = id_token.split('.');
|
let mut parts = id_token.split('.');
|
||||||
let (_header_b64, payload_b64, _sig_b64) = match (parts.next(), parts.next(), parts.next()) {
|
let (_header_b64, payload_b64, _sig_b64) = match (parts.next(), parts.next(), parts.next()) {
|
||||||
@@ -204,9 +204,33 @@ mod tests {
|
|||||||
|
|
||||||
let info = parse_id_token(&fake_jwt).expect("should parse");
|
let info = parse_id_token(&fake_jwt).expect("should parse");
|
||||||
assert_eq!(info.email.as_deref(), Some("user@example.com"));
|
assert_eq!(info.email.as_deref(), Some("user@example.com"));
|
||||||
assert_eq!(
|
assert_eq!(info.get_chatgpt_plan_type().as_deref(), Some("Pro"));
|
||||||
info.chatgpt_plan_type,
|
}
|
||||||
Some(PlanType::Known(KnownPlan::Pro))
|
|
||||||
);
|
#[test]
|
||||||
|
fn id_token_info_handles_missing_fields() {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Header {
|
||||||
|
alg: &'static str,
|
||||||
|
typ: &'static str,
|
||||||
|
}
|
||||||
|
let header = Header {
|
||||||
|
alg: "none",
|
||||||
|
typ: "JWT",
|
||||||
|
};
|
||||||
|
let payload = serde_json::json!({ "sub": "123" });
|
||||||
|
|
||||||
|
fn b64url_no_pad(bytes: &[u8]) -> String {
|
||||||
|
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_b64 = b64url_no_pad(&serde_json::to_vec(&header).unwrap());
|
||||||
|
let payload_b64 = b64url_no_pad(&serde_json::to_vec(&payload).unwrap());
|
||||||
|
let signature_b64 = b64url_no_pad(b"sig");
|
||||||
|
let fake_jwt = format!("{header_b64}.{payload_b64}.{signature_b64}");
|
||||||
|
|
||||||
|
let info = parse_id_token(&fake_jwt).expect("should parse");
|
||||||
|
assert!(info.email.is_none());
|
||||||
|
assert!(info.get_chatgpt_plan_type().is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use codex_core::CodexAuth;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::ModelProviderInfo;
|
use codex_core::ModelProviderInfo;
|
||||||
use codex_core::NewConversation;
|
use codex_core::NewConversation;
|
||||||
@@ -7,8 +8,7 @@ use codex_core::protocol::EventMsg;
|
|||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||||
use codex_login::AuthMode;
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
use codex_login::CodexAuth;
|
|
||||||
use core_test_support::load_default_config_for_test;
|
use core_test_support::load_default_config_for_test;
|
||||||
use core_test_support::load_sse_fixture_with_id;
|
use core_test_support::load_sse_fixture_with_id;
|
||||||
use core_test_support::wait_for_event;
|
use core_test_support::wait_for_event;
|
||||||
@@ -416,7 +416,7 @@ async fn prefers_chatgpt_token_when_config_prefers_chatgpt() {
|
|||||||
|
|
||||||
let auth_manager =
|
let auth_manager =
|
||||||
match CodexAuth::from_codex_home(codex_home.path(), config.preferred_auth_method) {
|
match CodexAuth::from_codex_home(codex_home.path(), config.preferred_auth_method) {
|
||||||
Ok(Some(auth)) => codex_login::AuthManager::from_auth_for_testing(auth),
|
Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth),
|
||||||
Ok(None) => panic!("No CodexAuth found in codex_home"),
|
Ok(None) => panic!("No CodexAuth found in codex_home"),
|
||||||
Err(e) => panic!("Failed to load CodexAuth: {e}"),
|
Err(e) => panic!("Failed to load CodexAuth: {e}"),
|
||||||
};
|
};
|
||||||
@@ -497,7 +497,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
|
|||||||
|
|
||||||
let auth_manager =
|
let auth_manager =
|
||||||
match CodexAuth::from_codex_home(codex_home.path(), config.preferred_auth_method) {
|
match CodexAuth::from_codex_home(codex_home.path(), config.preferred_auth_method) {
|
||||||
Ok(Some(auth)) => codex_login::AuthManager::from_auth_for_testing(auth),
|
Ok(Some(auth)) => codex_core::AuthManager::from_auth_for_testing(auth),
|
||||||
Ok(None) => panic!("No CodexAuth found in codex_home"),
|
Ok(None) => panic!("No CodexAuth found in codex_home"),
|
||||||
Err(e) => panic!("Failed to load CodexAuth: {e}"),
|
Err(e) => panic!("Failed to load CodexAuth: {e}"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#![expect(clippy::unwrap_used)]
|
#![expect(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use codex_core::CodexAuth;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::ModelProviderInfo;
|
use codex_core::ModelProviderInfo;
|
||||||
use codex_core::built_in_model_providers;
|
use codex_core::built_in_model_providers;
|
||||||
@@ -7,7 +8,6 @@ use codex_core::protocol::EventMsg;
|
|||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||||
use codex_login::CodexAuth;
|
|
||||||
use core_test_support::load_default_config_for_test;
|
use core_test_support::load_default_config_for_test;
|
||||||
use core_test_support::wait_for_event;
|
use core_test_support::wait_for_event;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use codex_core::CodexAuth;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::ModelProviderInfo;
|
use codex_core::ModelProviderInfo;
|
||||||
use codex_core::NewConversation;
|
use codex_core::NewConversation;
|
||||||
@@ -6,7 +7,6 @@ use codex_core::protocol::ConversationHistoryResponseEvent;
|
|||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
use codex_login::CodexAuth;
|
|
||||||
use core_test_support::load_default_config_for_test;
|
use core_test_support::load_default_config_for_test;
|
||||||
use core_test_support::wait_for_event;
|
use core_test_support::wait_for_event;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use codex_core::CodexAuth;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::ModelProviderInfo;
|
use codex_core::ModelProviderInfo;
|
||||||
use codex_core::built_in_model_providers;
|
use codex_core::built_in_model_providers;
|
||||||
@@ -12,7 +13,6 @@ use codex_core::protocol::SandboxPolicy;
|
|||||||
use codex_core::protocol_config_types::ReasoningEffort;
|
use codex_core::protocol_config_types::ReasoningEffort;
|
||||||
use codex_core::protocol_config_types::ReasoningSummary;
|
use codex_core::protocol_config_types::ReasoningSummary;
|
||||||
use codex_core::shell::default_user_shell;
|
use codex_core::shell::default_user_shell;
|
||||||
use codex_login::CodexAuth;
|
|
||||||
use core_test_support::load_default_config_for_test;
|
use core_test_support::load_default_config_for_test;
|
||||||
use core_test_support::load_sse_fixture_with_id;
|
use core_test_support::load_sse_fixture_with_id;
|
||||||
use core_test_support::wait_for_event;
|
use core_test_support::wait_for_event;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use codex_core::CodexAuth;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::ModelProviderInfo;
|
use codex_core::ModelProviderInfo;
|
||||||
use codex_core::WireApi;
|
use codex_core::WireApi;
|
||||||
@@ -7,7 +8,6 @@ use codex_core::protocol::EventMsg;
|
|||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||||
use codex_login::CodexAuth;
|
|
||||||
use core_test_support::load_default_config_for_test;
|
use core_test_support::load_default_config_for_test;
|
||||||
use core_test_support::load_sse_fixture_with_id;
|
use core_test_support::load_sse_fixture_with_id;
|
||||||
use core_test_support::wait_for_event_with_timeout;
|
use core_test_support::wait_for_event_with_timeout;
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use codex_core::CodexAuth;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::ModelProviderInfo;
|
use codex_core::ModelProviderInfo;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||||
use codex_login::CodexAuth;
|
|
||||||
use core_test_support::load_default_config_for_test;
|
use core_test_support::load_default_config_for_test;
|
||||||
use core_test_support::load_sse_fixture;
|
use core_test_support::load_sse_fixture;
|
||||||
use core_test_support::load_sse_fixture_with_id;
|
use core_test_support::load_sse_fixture_with_id;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use std::io::Read;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub use cli::Cli;
|
pub use cli::Cli;
|
||||||
|
use codex_core::AuthManager;
|
||||||
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::NewConversation;
|
use codex_core::NewConversation;
|
||||||
@@ -20,7 +21,6 @@ use codex_core::protocol::EventMsg;
|
|||||||
use codex_core::protocol::InputItem;
|
use codex_core::protocol::InputItem;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
use codex_core::protocol::TaskCompleteEvent;
|
use codex_core::protocol::TaskCompleteEvent;
|
||||||
use codex_login::AuthManager;
|
|
||||||
use codex_ollama::DEFAULT_OSS_MODEL;
|
use codex_ollama::DEFAULT_OSS_MODEL;
|
||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
use event_processor_with_human_output::EventProcessorWithHumanOutput;
|
use event_processor_with_human_output::EventProcessorWithHumanOutput;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
codex-core = { path = "../core" }
|
||||||
codex-protocol = { path = "../protocol" }
|
codex-protocol = { path = "../protocol" }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::RwLock;
|
|
||||||
|
|
||||||
use crate::AuthMode;
|
|
||||||
use crate::CodexAuth;
|
|
||||||
|
|
||||||
/// Internal cached auth state.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct CachedAuth {
|
|
||||||
preferred_auth_mode: AuthMode,
|
|
||||||
auth: Option<CodexAuth>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Central manager providing a single source of truth for auth.json derived
|
|
||||||
/// authentication data. It loads once (or on preference change) and then
|
|
||||||
/// hands out cloned `CodexAuth` values so the rest of the program has a
|
|
||||||
/// consistent snapshot.
|
|
||||||
///
|
|
||||||
/// External modifications to `auth.json` will NOT be observed until
|
|
||||||
/// `reload()` is called explicitly. This matches the design goal of avoiding
|
|
||||||
/// different parts of the program seeing inconsistent auth data mid‑run.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AuthManager {
|
|
||||||
codex_home: PathBuf,
|
|
||||||
inner: RwLock<CachedAuth>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthManager {
|
|
||||||
/// Create a new manager loading the initial auth using the provided
|
|
||||||
/// preferred auth method. Errors loading auth are swallowed; `auth()` will
|
|
||||||
/// simply return `None` in that case so callers can treat it as an
|
|
||||||
/// unauthenticated state.
|
|
||||||
pub fn new(codex_home: PathBuf, preferred_auth_mode: AuthMode) -> Self {
|
|
||||||
let auth = crate::CodexAuth::from_codex_home(&codex_home, preferred_auth_mode)
|
|
||||||
.ok()
|
|
||||||
.flatten();
|
|
||||||
Self {
|
|
||||||
codex_home,
|
|
||||||
inner: RwLock::new(CachedAuth {
|
|
||||||
preferred_auth_mode,
|
|
||||||
auth,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create an AuthManager with a specific CodexAuth, for testing only.
|
|
||||||
pub fn from_auth_for_testing(auth: CodexAuth) -> Arc<Self> {
|
|
||||||
let preferred_auth_mode = auth.mode;
|
|
||||||
let cached = CachedAuth {
|
|
||||||
preferred_auth_mode,
|
|
||||||
auth: Some(auth),
|
|
||||||
};
|
|
||||||
Arc::new(Self {
|
|
||||||
codex_home: PathBuf::new(),
|
|
||||||
inner: RwLock::new(cached),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Current cached auth (clone). May be `None` if not logged in or load failed.
|
|
||||||
pub fn auth(&self) -> Option<CodexAuth> {
|
|
||||||
self.inner.read().ok().and_then(|c| c.auth.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Preferred auth method used when (re)loading.
|
|
||||||
pub fn preferred_auth_method(&self) -> AuthMode {
|
|
||||||
self.inner
|
|
||||||
.read()
|
|
||||||
.map(|c| c.preferred_auth_mode)
|
|
||||||
.unwrap_or(AuthMode::ApiKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Force a reload using the existing preferred auth method. Returns
|
|
||||||
/// whether the auth value changed.
|
|
||||||
pub fn reload(&self) -> bool {
|
|
||||||
let preferred = self.preferred_auth_method();
|
|
||||||
let new_auth = crate::CodexAuth::from_codex_home(&self.codex_home, preferred)
|
|
||||||
.ok()
|
|
||||||
.flatten();
|
|
||||||
if let Ok(mut guard) = self.inner.write() {
|
|
||||||
let changed = !AuthManager::auths_equal(&guard.auth, &new_auth);
|
|
||||||
guard.auth = new_auth;
|
|
||||||
changed
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auths_equal(a: &Option<CodexAuth>, b: &Option<CodexAuth>) -> bool {
|
|
||||||
match (a, b) {
|
|
||||||
(None, None) => true,
|
|
||||||
(Some(a), Some(b)) => a == b,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience constructor returning an `Arc` wrapper.
|
|
||||||
pub fn shared(codex_home: PathBuf, preferred_auth_mode: AuthMode) -> Arc<Self> {
|
|
||||||
Arc::new(Self::new(codex_home, preferred_auth_mode))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to refresh the current auth token (if any). On success, reload
|
|
||||||
/// the auth state from disk so other components observe refreshed token.
|
|
||||||
pub async fn refresh_token(&self) -> std::io::Result<Option<String>> {
|
|
||||||
let auth = match self.auth() {
|
|
||||||
Some(a) => a,
|
|
||||||
None => return Ok(None),
|
|
||||||
};
|
|
||||||
match auth.refresh_token().await {
|
|
||||||
Ok(token) => {
|
|
||||||
// Reload to pick up persisted changes.
|
|
||||||
self.reload();
|
|
||||||
Ok(Some(token))
|
|
||||||
}
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log out by deleting the on‑disk auth.json (if present). Returns Ok(true)
|
|
||||||
/// if a file was removed, Ok(false) if no auth file existed. On success,
|
|
||||||
/// reloads the in‑memory auth cache so callers immediately observe the
|
|
||||||
/// unauthenticated state.
|
|
||||||
pub fn logout(&self) -> std::io::Result<bool> {
|
|
||||||
let removed = crate::logout(&self.codex_home)?;
|
|
||||||
// Always reload to clear any cached auth (even if file absent).
|
|
||||||
self.reload();
|
|
||||||
Ok(removed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,693 +1,21 @@
|
|||||||
use chrono::DateTime;
|
|
||||||
use chrono::Utc;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::env;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::fs::OpenOptions;
|
|
||||||
use std::fs::remove_file;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::io::Write;
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub use crate::server::LoginServer;
|
|
||||||
pub use crate::server::ServerOptions;
|
|
||||||
pub use crate::server::ShutdownHandle;
|
|
||||||
pub use crate::server::run_login_server;
|
|
||||||
pub use crate::token_data::TokenData;
|
|
||||||
use crate::token_data::parse_id_token;
|
|
||||||
|
|
||||||
mod auth_manager;
|
|
||||||
mod pkce;
|
mod pkce;
|
||||||
mod server;
|
mod server;
|
||||||
mod token_data;
|
|
||||||
|
|
||||||
pub const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
|
pub use server::LoginServer;
|
||||||
pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
|
pub use server::ServerOptions;
|
||||||
pub use auth_manager::AuthManager;
|
pub use server::ShutdownHandle;
|
||||||
|
pub use server::run_login_server;
|
||||||
|
|
||||||
|
// Re-export commonly used auth types and helpers from codex-core for compatibility
|
||||||
|
pub use codex_core::AuthManager;
|
||||||
|
pub use codex_core::CodexAuth;
|
||||||
|
pub use codex_core::auth::AuthDotJson;
|
||||||
|
pub use codex_core::auth::CLIENT_ID;
|
||||||
|
pub use codex_core::auth::OPENAI_API_KEY_ENV_VAR;
|
||||||
|
pub use codex_core::auth::get_auth_file;
|
||||||
|
pub use codex_core::auth::login_with_api_key;
|
||||||
|
pub use codex_core::auth::logout;
|
||||||
|
pub use codex_core::auth::try_read_auth_json;
|
||||||
|
pub use codex_core::auth::write_auth_json;
|
||||||
|
pub use codex_core::token_data::TokenData;
|
||||||
pub use codex_protocol::mcp_protocol::AuthMode;
|
pub use codex_protocol::mcp_protocol::AuthMode;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CodexAuth {
|
|
||||||
pub mode: AuthMode,
|
|
||||||
|
|
||||||
api_key: Option<String>,
|
|
||||||
auth_dot_json: Arc<Mutex<Option<AuthDotJson>>>,
|
|
||||||
auth_file: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for CodexAuth {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.mode == other.mode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CodexAuth {
|
|
||||||
pub fn from_api_key(api_key: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
api_key: Some(api_key.to_owned()),
|
|
||||||
mode: AuthMode::ApiKey,
|
|
||||||
auth_file: PathBuf::new(),
|
|
||||||
auth_dot_json: Arc::new(Mutex::new(None)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh_token(&self) -> Result<String, std::io::Error> {
|
|
||||||
let token_data = self
|
|
||||||
.get_current_token_data()
|
|
||||||
.ok_or(std::io::Error::other("Token data is not available."))?;
|
|
||||||
let token = token_data.refresh_token;
|
|
||||||
|
|
||||||
let refresh_response = try_refresh_token(token)
|
|
||||||
.await
|
|
||||||
.map_err(std::io::Error::other)?;
|
|
||||||
|
|
||||||
let updated = update_tokens(
|
|
||||||
&self.auth_file,
|
|
||||||
refresh_response.id_token,
|
|
||||||
refresh_response.access_token,
|
|
||||||
refresh_response.refresh_token,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let Ok(mut auth_lock) = self.auth_dot_json.lock() {
|
|
||||||
*auth_lock = Some(updated.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let access = match updated.tokens {
|
|
||||||
Some(t) => t.access_token,
|
|
||||||
None => {
|
|
||||||
return Err(std::io::Error::other(
|
|
||||||
"Token data is not available after refresh.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(access)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads the available auth information from the auth.json or
|
|
||||||
/// OPENAI_API_KEY environment variable.
|
|
||||||
pub fn from_codex_home(
|
|
||||||
codex_home: &Path,
|
|
||||||
preferred_auth_method: AuthMode,
|
|
||||||
) -> std::io::Result<Option<CodexAuth>> {
|
|
||||||
load_auth(codex_home, true, preferred_auth_method)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_token_data(&self) -> Result<TokenData, std::io::Error> {
|
|
||||||
let auth_dot_json: Option<AuthDotJson> = self.get_current_auth_json();
|
|
||||||
match auth_dot_json {
|
|
||||||
Some(AuthDotJson {
|
|
||||||
tokens: Some(mut tokens),
|
|
||||||
last_refresh: Some(last_refresh),
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
if last_refresh < Utc::now() - chrono::Duration::days(28) {
|
|
||||||
let refresh_response = tokio::time::timeout(
|
|
||||||
Duration::from_secs(60),
|
|
||||||
try_refresh_token(tokens.refresh_token.clone()),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| {
|
|
||||||
std::io::Error::other("timed out while refreshing OpenAI API key")
|
|
||||||
})?
|
|
||||||
.map_err(std::io::Error::other)?;
|
|
||||||
|
|
||||||
let updated_auth_dot_json = update_tokens(
|
|
||||||
&self.auth_file,
|
|
||||||
refresh_response.id_token,
|
|
||||||
refresh_response.access_token,
|
|
||||||
refresh_response.refresh_token,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
tokens = updated_auth_dot_json
|
|
||||||
.tokens
|
|
||||||
.clone()
|
|
||||||
.ok_or(std::io::Error::other(
|
|
||||||
"Token data is not available after refresh.",
|
|
||||||
))?;
|
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
|
||||||
let mut auth_lock = self.auth_dot_json.lock().unwrap();
|
|
||||||
*auth_lock = Some(updated_auth_dot_json);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tokens)
|
|
||||||
}
|
|
||||||
_ => Err(std::io::Error::other("Token data is not available.")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_token(&self) -> Result<String, std::io::Error> {
|
|
||||||
match self.mode {
|
|
||||||
AuthMode::ApiKey => Ok(self.api_key.clone().unwrap_or_default()),
|
|
||||||
AuthMode::ChatGPT => {
|
|
||||||
let id_token = self.get_token_data().await?.access_token;
|
|
||||||
|
|
||||||
Ok(id_token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_account_id(&self) -> Option<String> {
|
|
||||||
self.get_current_token_data()
|
|
||||||
.and_then(|t| t.account_id.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_plan_type(&self) -> Option<String> {
|
|
||||||
self.get_current_token_data()
|
|
||||||
.and_then(|t| t.id_token.chatgpt_plan_type.as_ref().map(|p| p.as_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_current_auth_json(&self) -> Option<AuthDotJson> {
|
|
||||||
#[expect(clippy::unwrap_used)]
|
|
||||||
self.auth_dot_json.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_current_token_data(&self) -> Option<TokenData> {
|
|
||||||
self.get_current_auth_json().and_then(|t| t.tokens.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Consider this private to integration tests.
|
|
||||||
pub fn create_dummy_chatgpt_auth_for_testing() -> Self {
|
|
||||||
let auth_dot_json = AuthDotJson {
|
|
||||||
openai_api_key: None,
|
|
||||||
tokens: Some(TokenData {
|
|
||||||
id_token: Default::default(),
|
|
||||||
access_token: "Access Token".to_string(),
|
|
||||||
refresh_token: "test".to_string(),
|
|
||||||
account_id: Some("account_id".to_string()),
|
|
||||||
}),
|
|
||||||
last_refresh: Some(Utc::now()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let auth_dot_json = Arc::new(Mutex::new(Some(auth_dot_json)));
|
|
||||||
Self {
|
|
||||||
api_key: None,
|
|
||||||
mode: AuthMode::ChatGPT,
|
|
||||||
auth_file: PathBuf::new(),
|
|
||||||
auth_dot_json,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_auth(
|
|
||||||
codex_home: &Path,
|
|
||||||
include_env_var: bool,
|
|
||||||
preferred_auth_method: AuthMode,
|
|
||||||
) -> std::io::Result<Option<CodexAuth>> {
|
|
||||||
// First, check to see if there is a valid auth.json file. If not, we fall
|
|
||||||
// back to AuthMode::ApiKey using the OPENAI_API_KEY environment variable
|
|
||||||
// (if it is set).
|
|
||||||
let auth_file = get_auth_file(codex_home);
|
|
||||||
let auth_dot_json = match try_read_auth_json(&auth_file) {
|
|
||||||
Ok(auth) => auth,
|
|
||||||
// If auth.json does not exist, try to read the OPENAI_API_KEY from the
|
|
||||||
// environment variable.
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound && include_env_var => {
|
|
||||||
return match read_openai_api_key_from_env() {
|
|
||||||
Some(api_key) => Ok(Some(CodexAuth::from_api_key(&api_key))),
|
|
||||||
None => Ok(None),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Though if auth.json exists but is malformed, do not fall back to the
|
|
||||||
// env var because the user may be expecting to use AuthMode::ChatGPT.
|
|
||||||
Err(e) => {
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let AuthDotJson {
|
|
||||||
openai_api_key: auth_json_api_key,
|
|
||||||
tokens,
|
|
||||||
last_refresh,
|
|
||||||
} = auth_dot_json;
|
|
||||||
|
|
||||||
// If the auth.json has an API key AND does not appear to be on a plan that
|
|
||||||
// should prefer AuthMode::ChatGPT, use AuthMode::ApiKey.
|
|
||||||
if let Some(api_key) = &auth_json_api_key {
|
|
||||||
// Should any of these be AuthMode::ChatGPT with the api_key set?
|
|
||||||
// Does AuthMode::ChatGPT indicate that there is an auth.json that is
|
|
||||||
// "refreshable" even if we are using the API key for auth?
|
|
||||||
match &tokens {
|
|
||||||
Some(tokens) => {
|
|
||||||
if tokens.should_use_api_key(preferred_auth_method, tokens.is_openai_email()) {
|
|
||||||
return Ok(Some(CodexAuth::from_api_key(api_key)));
|
|
||||||
} else {
|
|
||||||
// Ignore the API key and fall through to ChatGPT auth.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// We have an API key but no tokens in the auth.json file.
|
|
||||||
// Perhaps the user ran `codex login --api-key <KEY>` or updated
|
|
||||||
// auth.json by hand. Either way, let's assume they are trying
|
|
||||||
// to use their API key.
|
|
||||||
return Ok(Some(CodexAuth::from_api_key(api_key)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the AuthMode::ChatGPT variant, perhaps neither api_key nor
|
|
||||||
// openai_api_key should exist?
|
|
||||||
Ok(Some(CodexAuth {
|
|
||||||
api_key: None,
|
|
||||||
mode: AuthMode::ChatGPT,
|
|
||||||
auth_file,
|
|
||||||
auth_dot_json: Arc::new(Mutex::new(Some(AuthDotJson {
|
|
||||||
openai_api_key: None,
|
|
||||||
tokens,
|
|
||||||
last_refresh,
|
|
||||||
}))),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_openai_api_key_from_env() -> Option<String> {
|
|
||||||
env::var(OPENAI_API_KEY_ENV_VAR)
|
|
||||||
.ok()
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_auth_file(codex_home: &Path) -> PathBuf {
|
|
||||||
codex_home.join("auth.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete the auth.json file inside `codex_home` if it exists. Returns `Ok(true)`
|
|
||||||
/// if a file was removed, `Ok(false)` if no auth file was present.
|
|
||||||
pub fn logout(codex_home: &Path) -> std::io::Result<bool> {
|
|
||||||
let auth_file = get_auth_file(codex_home);
|
|
||||||
match remove_file(&auth_file) {
|
|
||||||
Ok(_) => Ok(true),
|
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn login_with_api_key(codex_home: &Path, api_key: &str) -> std::io::Result<()> {
|
|
||||||
let auth_dot_json = AuthDotJson {
|
|
||||||
openai_api_key: Some(api_key.to_string()),
|
|
||||||
tokens: None,
|
|
||||||
last_refresh: None,
|
|
||||||
};
|
|
||||||
write_auth_json(&get_auth_file(codex_home), &auth_dot_json)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to read and refresh the `auth.json` file in the given `CODEX_HOME` directory.
|
|
||||||
/// Returns the full AuthDotJson structure after refreshing if necessary.
|
|
||||||
pub fn try_read_auth_json(auth_file: &Path) -> std::io::Result<AuthDotJson> {
|
|
||||||
let mut file = File::open(auth_file)?;
|
|
||||||
let mut contents = String::new();
|
|
||||||
file.read_to_string(&mut contents)?;
|
|
||||||
let auth_dot_json: AuthDotJson = serde_json::from_str(&contents)?;
|
|
||||||
|
|
||||||
Ok(auth_dot_json)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_auth_json(auth_file: &Path, auth_dot_json: &AuthDotJson) -> std::io::Result<()> {
|
|
||||||
let json_data = serde_json::to_string_pretty(auth_dot_json)?;
|
|
||||||
let mut options = OpenOptions::new();
|
|
||||||
options.truncate(true).write(true).create(true);
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
options.mode(0o600);
|
|
||||||
}
|
|
||||||
let mut file = options.open(auth_file)?;
|
|
||||||
file.write_all(json_data.as_bytes())?;
|
|
||||||
file.flush()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_tokens(
|
|
||||||
auth_file: &Path,
|
|
||||||
id_token: String,
|
|
||||||
access_token: Option<String>,
|
|
||||||
refresh_token: Option<String>,
|
|
||||||
) -> std::io::Result<AuthDotJson> {
|
|
||||||
let mut auth_dot_json = try_read_auth_json(auth_file)?;
|
|
||||||
|
|
||||||
let tokens = auth_dot_json.tokens.get_or_insert_with(TokenData::default);
|
|
||||||
tokens.id_token = parse_id_token(&id_token).map_err(std::io::Error::other)?;
|
|
||||||
if let Some(access_token) = access_token {
|
|
||||||
tokens.access_token = access_token.to_string();
|
|
||||||
}
|
|
||||||
if let Some(refresh_token) = refresh_token {
|
|
||||||
tokens.refresh_token = refresh_token.to_string();
|
|
||||||
}
|
|
||||||
auth_dot_json.last_refresh = Some(Utc::now());
|
|
||||||
write_auth_json(auth_file, &auth_dot_json)?;
|
|
||||||
Ok(auth_dot_json)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_refresh_token(refresh_token: String) -> std::io::Result<RefreshResponse> {
|
|
||||||
let refresh_request = RefreshRequest {
|
|
||||||
client_id: CLIENT_ID,
|
|
||||||
grant_type: "refresh_token",
|
|
||||||
refresh_token,
|
|
||||||
scope: "openid profile email",
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let response = client
|
|
||||||
.post("https://auth.openai.com/oauth/token")
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.json(&refresh_request)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(std::io::Error::other)?;
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
let refresh_response = response
|
|
||||||
.json::<RefreshResponse>()
|
|
||||||
.await
|
|
||||||
.map_err(std::io::Error::other)?;
|
|
||||||
Ok(refresh_response)
|
|
||||||
} else {
|
|
||||||
Err(std::io::Error::other(format!(
|
|
||||||
"Failed to refresh token: {}",
|
|
||||||
response.status()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct RefreshRequest {
|
|
||||||
client_id: &'static str,
|
|
||||||
grant_type: &'static str,
|
|
||||||
refresh_token: String,
|
|
||||||
scope: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
struct RefreshResponse {
|
|
||||||
id_token: String,
|
|
||||||
access_token: Option<String>,
|
|
||||||
refresh_token: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Expected structure for $CODEX_HOME/auth.json.
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
|
||||||
pub struct AuthDotJson {
|
|
||||||
#[serde(rename = "OPENAI_API_KEY")]
|
|
||||||
pub openai_api_key: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub tokens: Option<TokenData>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub last_refresh: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::token_data::IdTokenInfo;
|
|
||||||
use crate::token_data::KnownPlan;
|
|
||||||
use crate::token_data::PlanType;
|
|
||||||
use base64::Engine;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
use serde_json::json;
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
const LAST_REFRESH: &str = "2025-08-06T20:41:36.232376Z";
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn writes_api_key_and_loads_auth() {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
login_with_api_key(dir.path(), "sk-test-key").unwrap();
|
|
||||||
let auth = load_auth(dir.path(), false, AuthMode::ChatGPT)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(auth.mode, AuthMode::ApiKey);
|
|
||||||
assert_eq!(auth.api_key.as_deref(), Some("sk-test-key"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn loads_from_env_var_if_env_var_exists() {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
|
|
||||||
let env_var = std::env::var(OPENAI_API_KEY_ENV_VAR);
|
|
||||||
|
|
||||||
if let Ok(env_var) = env_var {
|
|
||||||
let auth = load_auth(dir.path(), true, AuthMode::ChatGPT)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(auth.mode, AuthMode::ApiKey);
|
|
||||||
assert_eq!(auth.api_key, Some(env_var));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn roundtrip_auth_dot_json() {
|
|
||||||
let codex_home = tempdir().unwrap();
|
|
||||||
write_auth_file(
|
|
||||||
AuthFileParams {
|
|
||||||
openai_api_key: None,
|
|
||||||
chatgpt_plan_type: "pro".to_string(),
|
|
||||||
},
|
|
||||||
codex_home.path(),
|
|
||||||
)
|
|
||||||
.expect("failed to write auth file");
|
|
||||||
|
|
||||||
let file = get_auth_file(codex_home.path());
|
|
||||||
let auth_dot_json = try_read_auth_json(&file).unwrap();
|
|
||||||
write_auth_json(&file, &auth_dot_json).unwrap();
|
|
||||||
|
|
||||||
let same_auth_dot_json = try_read_auth_json(&file).unwrap();
|
|
||||||
assert_eq!(auth_dot_json, same_auth_dot_json);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
|
||||||
let codex_home = tempdir().unwrap();
|
|
||||||
let fake_jwt = write_auth_file(
|
|
||||||
AuthFileParams {
|
|
||||||
openai_api_key: None,
|
|
||||||
chatgpt_plan_type: "pro".to_string(),
|
|
||||||
},
|
|
||||||
codex_home.path(),
|
|
||||||
)
|
|
||||||
.expect("failed to write auth file");
|
|
||||||
|
|
||||||
let CodexAuth {
|
|
||||||
api_key,
|
|
||||||
mode,
|
|
||||||
auth_dot_json,
|
|
||||||
auth_file: _,
|
|
||||||
} = load_auth(codex_home.path(), false, AuthMode::ChatGPT)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(None, api_key);
|
|
||||||
assert_eq!(AuthMode::ChatGPT, mode);
|
|
||||||
|
|
||||||
let guard = auth_dot_json.lock().unwrap();
|
|
||||||
let auth_dot_json = guard.as_ref().expect("AuthDotJson should exist");
|
|
||||||
assert_eq!(
|
|
||||||
&AuthDotJson {
|
|
||||||
openai_api_key: None,
|
|
||||||
tokens: Some(TokenData {
|
|
||||||
id_token: IdTokenInfo {
|
|
||||||
email: Some("user@example.com".to_string()),
|
|
||||||
chatgpt_plan_type: Some(PlanType::Known(KnownPlan::Pro)),
|
|
||||||
raw_jwt: fake_jwt,
|
|
||||||
},
|
|
||||||
access_token: "test-access-token".to_string(),
|
|
||||||
refresh_token: "test-refresh-token".to_string(),
|
|
||||||
account_id: None,
|
|
||||||
}),
|
|
||||||
last_refresh: Some(
|
|
||||||
DateTime::parse_from_rfc3339(LAST_REFRESH)
|
|
||||||
.unwrap()
|
|
||||||
.with_timezone(&Utc)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
auth_dot_json
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Even if the OPENAI_API_KEY is set in auth.json, if the plan is not in
|
|
||||||
/// [`TokenData::is_plan_that_should_use_api_key`], it should use
|
|
||||||
/// [`AuthMode::ChatGPT`].
|
|
||||||
#[tokio::test]
|
|
||||||
async fn pro_account_with_api_key_still_uses_chatgpt_auth() {
|
|
||||||
let codex_home = tempdir().unwrap();
|
|
||||||
let fake_jwt = write_auth_file(
|
|
||||||
AuthFileParams {
|
|
||||||
openai_api_key: Some("sk-test-key".to_string()),
|
|
||||||
chatgpt_plan_type: "pro".to_string(),
|
|
||||||
},
|
|
||||||
codex_home.path(),
|
|
||||||
)
|
|
||||||
.expect("failed to write auth file");
|
|
||||||
|
|
||||||
let CodexAuth {
|
|
||||||
api_key,
|
|
||||||
mode,
|
|
||||||
auth_dot_json,
|
|
||||||
auth_file: _,
|
|
||||||
} = load_auth(codex_home.path(), false, AuthMode::ChatGPT)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(None, api_key);
|
|
||||||
assert_eq!(AuthMode::ChatGPT, mode);
|
|
||||||
|
|
||||||
let guard = auth_dot_json.lock().unwrap();
|
|
||||||
let auth_dot_json = guard.as_ref().expect("AuthDotJson should exist");
|
|
||||||
assert_eq!(
|
|
||||||
&AuthDotJson {
|
|
||||||
openai_api_key: None,
|
|
||||||
tokens: Some(TokenData {
|
|
||||||
id_token: IdTokenInfo {
|
|
||||||
email: Some("user@example.com".to_string()),
|
|
||||||
chatgpt_plan_type: Some(PlanType::Known(KnownPlan::Pro)),
|
|
||||||
raw_jwt: fake_jwt,
|
|
||||||
},
|
|
||||||
access_token: "test-access-token".to_string(),
|
|
||||||
refresh_token: "test-refresh-token".to_string(),
|
|
||||||
account_id: None,
|
|
||||||
}),
|
|
||||||
last_refresh: Some(
|
|
||||||
DateTime::parse_from_rfc3339(LAST_REFRESH)
|
|
||||||
.unwrap()
|
|
||||||
.with_timezone(&Utc)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
auth_dot_json
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the OPENAI_API_KEY is set in auth.json and it is an enterprise
|
|
||||||
/// account, then it should use [`AuthMode::ApiKey`].
|
|
||||||
#[tokio::test]
|
|
||||||
async fn enterprise_account_with_api_key_uses_chatgpt_auth() {
|
|
||||||
let codex_home = tempdir().unwrap();
|
|
||||||
write_auth_file(
|
|
||||||
AuthFileParams {
|
|
||||||
openai_api_key: Some("sk-test-key".to_string()),
|
|
||||||
chatgpt_plan_type: "enterprise".to_string(),
|
|
||||||
},
|
|
||||||
codex_home.path(),
|
|
||||||
)
|
|
||||||
.expect("failed to write auth file");
|
|
||||||
|
|
||||||
let CodexAuth {
|
|
||||||
api_key,
|
|
||||||
mode,
|
|
||||||
auth_dot_json,
|
|
||||||
auth_file: _,
|
|
||||||
} = load_auth(codex_home.path(), false, AuthMode::ChatGPT)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(Some("sk-test-key".to_string()), api_key);
|
|
||||||
assert_eq!(AuthMode::ApiKey, mode);
|
|
||||||
|
|
||||||
let guard = auth_dot_json.lock().expect("should unwrap");
|
|
||||||
assert!(guard.is_none(), "auth_dot_json should be None");
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AuthFileParams {
|
|
||||||
openai_api_key: Option<String>,
|
|
||||||
chatgpt_plan_type: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_auth_file(params: AuthFileParams, codex_home: &Path) -> std::io::Result<String> {
|
|
||||||
let auth_file = get_auth_file(codex_home);
|
|
||||||
// Create a minimal valid JWT for the id_token field.
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Header {
|
|
||||||
alg: &'static str,
|
|
||||||
typ: &'static str,
|
|
||||||
}
|
|
||||||
let header = Header {
|
|
||||||
alg: "none",
|
|
||||||
typ: "JWT",
|
|
||||||
};
|
|
||||||
let payload = serde_json::json!({
|
|
||||||
"email": "user@example.com",
|
|
||||||
"email_verified": true,
|
|
||||||
"https://api.openai.com/auth": {
|
|
||||||
"chatgpt_account_id": "bc3618e3-489d-4d49-9362-1561dc53ba53",
|
|
||||||
"chatgpt_plan_type": params.chatgpt_plan_type,
|
|
||||||
"chatgpt_user_id": "user-12345",
|
|
||||||
"user_id": "user-12345",
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let b64 = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b);
|
|
||||||
let header_b64 = b64(&serde_json::to_vec(&header)?);
|
|
||||||
let payload_b64 = b64(&serde_json::to_vec(&payload)?);
|
|
||||||
let signature_b64 = b64(b"sig");
|
|
||||||
let fake_jwt = format!("{header_b64}.{payload_b64}.{signature_b64}");
|
|
||||||
|
|
||||||
let auth_json_data = json!({
|
|
||||||
"OPENAI_API_KEY": params.openai_api_key,
|
|
||||||
"tokens": {
|
|
||||||
"id_token": fake_jwt,
|
|
||||||
"access_token": "test-access-token",
|
|
||||||
"refresh_token": "test-refresh-token"
|
|
||||||
},
|
|
||||||
"last_refresh": LAST_REFRESH,
|
|
||||||
});
|
|
||||||
let auth_json = serde_json::to_string_pretty(&auth_json_data)?;
|
|
||||||
std::fs::write(auth_file, auth_json)?;
|
|
||||||
|
|
||||||
Ok(fake_jwt)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn id_token_info_handles_missing_fields() {
|
|
||||||
// Payload without email or plan should yield None values.
|
|
||||||
let header = serde_json::json!({"alg": "none", "typ": "JWT"});
|
|
||||||
let payload = serde_json::json!({"sub": "123"});
|
|
||||||
let header_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
||||||
.encode(serde_json::to_vec(&header).unwrap());
|
|
||||||
let payload_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
||||||
.encode(serde_json::to_vec(&payload).unwrap());
|
|
||||||
let signature_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"sig");
|
|
||||||
let jwt = format!("{header_b64}.{payload_b64}.{signature_b64}");
|
|
||||||
|
|
||||||
let info = parse_id_token(&jwt).expect("should parse");
|
|
||||||
assert!(info.email.is_none());
|
|
||||||
assert!(info.chatgpt_plan_type.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn loads_api_key_from_auth_json() {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let auth_file = dir.path().join("auth.json");
|
|
||||||
std::fs::write(
|
|
||||||
auth_file,
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"OPENAI_API_KEY": "sk-test-key",
|
|
||||||
"tokens": null,
|
|
||||||
"last_refresh": null
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let auth = load_auth(dir.path(), false, AuthMode::ChatGPT)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(auth.mode, AuthMode::ApiKey);
|
|
||||||
assert_eq!(auth.api_key, Some("sk-test-key".to_string()));
|
|
||||||
|
|
||||||
assert!(auth.get_token_data().await.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logout_removes_auth_file() -> Result<(), std::io::Error> {
|
|
||||||
let dir = tempdir()?;
|
|
||||||
login_with_api_key(dir.path(), "sk-test-key")?;
|
|
||||||
assert!(dir.path().join("auth.json").exists());
|
|
||||||
let removed = logout(dir.path())?;
|
|
||||||
assert!(removed);
|
|
||||||
assert!(!dir.path().join("auth.json").exists());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ use std::path::PathBuf;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use crate::AuthDotJson;
|
|
||||||
use crate::get_auth_file;
|
|
||||||
use crate::pkce::PkceCodes;
|
use crate::pkce::PkceCodes;
|
||||||
use crate::pkce::generate_pkce;
|
use crate::pkce::generate_pkce;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use codex_core::auth::AuthDotJson;
|
||||||
|
use codex_core::auth::get_auth_file;
|
||||||
|
use codex_core::token_data::TokenData;
|
||||||
|
use codex_core::token_data::parse_id_token;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use tiny_http::Header;
|
use tiny_http::Header;
|
||||||
use tiny_http::Request;
|
use tiny_http::Request;
|
||||||
@@ -374,10 +376,8 @@ async fn persist_tokens_async(
|
|||||||
if let Some(key) = api_key {
|
if let Some(key) = api_key {
|
||||||
auth.openai_api_key = Some(key);
|
auth.openai_api_key = Some(key);
|
||||||
}
|
}
|
||||||
let tokens = auth
|
let tokens = auth.tokens.get_or_insert_with(TokenData::default);
|
||||||
.tokens
|
tokens.id_token = parse_id_token(&id_token).map_err(io::Error::other)?;
|
||||||
.get_or_insert_with(crate::token_data::TokenData::default);
|
|
||||||
tokens.id_token = crate::token_data::parse_id_token(&id_token).map_err(io::Error::other)?;
|
|
||||||
// Persist chatgpt_account_id if present in claims
|
// Persist chatgpt_account_id if present in claims
|
||||||
if let Some(acc) = jwt_auth_claims(&id_token)
|
if let Some(acc) = jwt_auth_claims(&id_token)
|
||||||
.get("chatgpt_account_id")
|
.get("chatgpt_account_id")
|
||||||
@@ -392,14 +392,14 @@ async fn persist_tokens_async(
|
|||||||
tokens.refresh_token = rt;
|
tokens.refresh_token = rt;
|
||||||
}
|
}
|
||||||
auth.last_refresh = Some(Utc::now());
|
auth.last_refresh = Some(Utc::now());
|
||||||
super::write_auth_json(&auth_file, &auth)
|
codex_core::auth::write_auth_json(&auth_file, &auth)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| io::Error::other(format!("persist task failed: {e}")))?
|
.map_err(|e| io::Error::other(format!("persist task failed: {e}")))?
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_or_default(path: &Path) -> AuthDotJson {
|
fn read_or_default(path: &Path) -> AuthDotJson {
|
||||||
match super::try_read_auth_json(path) {
|
match codex_core::auth::try_read_auth_json(path) {
|
||||||
Ok(auth) => auth,
|
Ok(auth) => auth,
|
||||||
Err(_) => AuthDotJson {
|
Err(_) => AuthDotJson {
|
||||||
openai_api_key: None,
|
openai_api_key: None,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::path::PathBuf;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use codex_core::AuthManager;
|
||||||
use codex_core::CodexConversation;
|
use codex_core::CodexConversation;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::NewConversation;
|
use codex_core::NewConversation;
|
||||||
@@ -16,7 +17,6 @@ use codex_core::protocol::Event;
|
|||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
use codex_core::protocol::ExecApprovalRequestEvent;
|
use codex_core::protocol::ExecApprovalRequestEvent;
|
||||||
use codex_core::protocol::ReviewDecision;
|
use codex_core::protocol::ReviewDecision;
|
||||||
use codex_login::AuthManager;
|
|
||||||
use codex_protocol::mcp_protocol::AuthMode;
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
use codex_protocol::mcp_protocol::GitDiffToRemoteResponse;
|
use codex_protocol::mcp_protocol::GitDiffToRemoteResponse;
|
||||||
use mcp_types::JSONRPCErrorError;
|
use mcp_types::JSONRPCErrorError;
|
||||||
@@ -31,9 +31,9 @@ use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
|||||||
use crate::json_to_toml::json_to_toml;
|
use crate::json_to_toml::json_to_toml;
|
||||||
use crate::outgoing_message::OutgoingMessageSender;
|
use crate::outgoing_message::OutgoingMessageSender;
|
||||||
use crate::outgoing_message::OutgoingNotification;
|
use crate::outgoing_message::OutgoingNotification;
|
||||||
|
use codex_core::auth::CLIENT_ID;
|
||||||
use codex_core::protocol::InputItem as CoreInputItem;
|
use codex_core::protocol::InputItem as CoreInputItem;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
use codex_login::CLIENT_ID;
|
|
||||||
use codex_login::ServerOptions as LoginServerOptions;
|
use codex_login::ServerOptions as LoginServerOptions;
|
||||||
use codex_login::ShutdownHandle;
|
use codex_login::ShutdownHandle;
|
||||||
use codex_login::run_login_server;
|
use codex_login::run_login_server;
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
|||||||
use crate::outgoing_message::OutgoingMessageSender;
|
use crate::outgoing_message::OutgoingMessageSender;
|
||||||
use codex_protocol::mcp_protocol::ClientRequest;
|
use codex_protocol::mcp_protocol::ClientRequest;
|
||||||
|
|
||||||
|
use codex_core::AuthManager;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::protocol::Submission;
|
use codex_core::protocol::Submission;
|
||||||
use codex_login::AuthManager;
|
|
||||||
use mcp_types::CallToolRequestParams;
|
use mcp_types::CallToolRequestParams;
|
||||||
use mcp_types::CallToolResult;
|
use mcp_types::CallToolResult;
|
||||||
use mcp_types::ClientRequest as McpClientRequest;
|
use mcp_types::ClientRequest as McpClientRequest;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use codex_login::login_with_api_key;
|
use codex_core::auth::login_with_api_key;
|
||||||
use codex_protocol::mcp_protocol::AuthMode;
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
use codex_protocol::mcp_protocol::GetAuthStatusParams;
|
use codex_protocol::mcp_protocol::GetAuthStatusParams;
|
||||||
use codex_protocol::mcp_protocol::GetAuthStatusResponse;
|
use codex_protocol::mcp_protocol::GetAuthStatusResponse;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use codex_login::login_with_api_key;
|
use codex_core::auth::login_with_api_key;
|
||||||
use codex_protocol::mcp_protocol::CancelLoginChatGptParams;
|
use codex_protocol::mcp_protocol::CancelLoginChatGptParams;
|
||||||
use codex_protocol::mcp_protocol::CancelLoginChatGptResponse;
|
use codex_protocol::mcp_protocol::CancelLoginChatGptResponse;
|
||||||
use codex_protocol::mcp_protocol::GetAuthStatusParams;
|
use codex_protocol::mcp_protocol::GetAuthStatusParams;
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ use crate::pager_overlay::Overlay;
|
|||||||
use crate::tui;
|
use crate::tui;
|
||||||
use crate::tui::TuiEvent;
|
use crate::tui::TuiEvent;
|
||||||
use codex_ansi_escape::ansi_escape_line;
|
use codex_ansi_escape::ansi_escape_line;
|
||||||
|
use codex_core::AuthManager;
|
||||||
use codex_core::ConversationManager;
|
use codex_core::ConversationManager;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::protocol::TokenUsage;
|
use codex_core::protocol::TokenUsage;
|
||||||
use codex_login::AuthManager;
|
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
|
|||||||
@@ -2042,7 +2042,7 @@ mod tests {
|
|||||||
composer.handle_paste(paste.clone());
|
composer.handle_paste(paste.clone());
|
||||||
composer
|
composer
|
||||||
.textarea
|
.textarea
|
||||||
.set_cursor((placeholder.len() - pos_from_end) as usize);
|
.set_cursor(placeholder.len() - pos_from_end);
|
||||||
composer.handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
|
composer.handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
|
||||||
let result = (
|
let result = (
|
||||||
composer.textarea.text().contains(&placeholder),
|
composer.textarea.text().contains(&placeholder),
|
||||||
|
|||||||
@@ -798,7 +798,7 @@ impl ChatWidget {
|
|||||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||||
}
|
}
|
||||||
SlashCommand::Logout => {
|
SlashCommand::Logout => {
|
||||||
if let Err(e) = codex_login::logout(&self.config.codex_home) {
|
if let Err(e) = codex_core::auth::logout(&self.config.codex_home) {
|
||||||
tracing::error!("failed to logout: {e}");
|
tracing::error!("failed to logout: {e}");
|
||||||
}
|
}
|
||||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::app_event::AppEvent;
|
use crate::app_event::AppEvent;
|
||||||
use crate::app_event_sender::AppEventSender;
|
use crate::app_event_sender::AppEventSender;
|
||||||
|
use codex_core::CodexAuth;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::config::ConfigOverrides;
|
use codex_core::config::ConfigOverrides;
|
||||||
use codex_core::config::ConfigToml;
|
use codex_core::config::ConfigToml;
|
||||||
@@ -23,7 +24,6 @@ use codex_core::protocol::PatchApplyEndEvent;
|
|||||||
use codex_core::protocol::StreamErrorEvent;
|
use codex_core::protocol::StreamErrorEvent;
|
||||||
use codex_core::protocol::TaskCompleteEvent;
|
use codex_core::protocol::TaskCompleteEvent;
|
||||||
use codex_core::protocol::TaskStartedEvent;
|
use codex_core::protocol::TaskStartedEvent;
|
||||||
use codex_login::CodexAuth;
|
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use crossterm::event::KeyModifiers;
|
use crossterm::event::KeyModifiers;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use base64::Engine;
|
|||||||
use codex_ansi_escape::ansi_escape_line;
|
use codex_ansi_escape::ansi_escape_line;
|
||||||
use codex_common::create_config_summary_entries;
|
use codex_common::create_config_summary_entries;
|
||||||
use codex_common::elapsed::format_duration;
|
use codex_common::elapsed::format_duration;
|
||||||
|
use codex_core::auth::get_auth_file;
|
||||||
|
use codex_core::auth::try_read_auth_json;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::plan_tool::PlanItemArg;
|
use codex_core::plan_tool::PlanItemArg;
|
||||||
use codex_core::plan_tool::StepStatus;
|
use codex_core::plan_tool::StepStatus;
|
||||||
@@ -18,8 +20,6 @@ use codex_core::protocol::McpInvocation;
|
|||||||
use codex_core::protocol::SandboxPolicy;
|
use codex_core::protocol::SandboxPolicy;
|
||||||
use codex_core::protocol::SessionConfiguredEvent;
|
use codex_core::protocol::SessionConfiguredEvent;
|
||||||
use codex_core::protocol::TokenUsage;
|
use codex_core::protocol::TokenUsage;
|
||||||
use codex_login::get_auth_file;
|
|
||||||
use codex_login::try_read_auth_json;
|
|
||||||
use codex_protocol::parse_command::ParsedCommand;
|
use codex_protocol::parse_command::ParsedCommand;
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use image::ImageReader;
|
use image::ImageReader;
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||||
#![deny(clippy::disallowed_methods)]
|
#![deny(clippy::disallowed_methods)]
|
||||||
use app::App;
|
use app::App;
|
||||||
|
use codex_core::AuthManager;
|
||||||
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
||||||
|
use codex_core::CodexAuth;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::config::ConfigOverrides;
|
use codex_core::config::ConfigOverrides;
|
||||||
use codex_core::config::ConfigToml;
|
use codex_core::config::ConfigToml;
|
||||||
@@ -12,11 +14,9 @@ use codex_core::config::find_codex_home;
|
|||||||
use codex_core::config::load_config_as_toml_with_cli_overrides;
|
use codex_core::config::load_config_as_toml_with_cli_overrides;
|
||||||
use codex_core::protocol::AskForApproval;
|
use codex_core::protocol::AskForApproval;
|
||||||
use codex_core::protocol::SandboxPolicy;
|
use codex_core::protocol::SandboxPolicy;
|
||||||
use codex_login::AuthManager;
|
|
||||||
use codex_login::AuthMode;
|
|
||||||
use codex_login::CodexAuth;
|
|
||||||
use codex_ollama::DEFAULT_OSS_MODEL;
|
use codex_ollama::DEFAULT_OSS_MODEL;
|
||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
use codex_login::AuthManager;
|
use codex_core::AuthManager;
|
||||||
use codex_login::CLIENT_ID;
|
use codex_core::auth::CLIENT_ID;
|
||||||
use codex_login::ServerOptions;
|
use codex_login::ServerOptions;
|
||||||
use codex_login::ShutdownHandle;
|
use codex_login::ShutdownHandle;
|
||||||
use codex_login::run_login_server;
|
use codex_login::run_login_server;
|
||||||
@@ -19,7 +19,7 @@ use ratatui::widgets::Paragraph;
|
|||||||
use ratatui::widgets::WidgetRef;
|
use ratatui::widgets::WidgetRef;
|
||||||
use ratatui::widgets::Wrap;
|
use ratatui::widgets::Wrap;
|
||||||
|
|
||||||
use codex_login::AuthMode;
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use crate::LoginStatus;
|
use crate::LoginStatus;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
use codex_core::AuthManager;
|
||||||
use codex_core::git_info::get_git_repo_root;
|
use codex_core::git_info::get_git_repo_root;
|
||||||
use codex_login::AuthManager;
|
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use crossterm::event::KeyEventKind;
|
use crossterm::event::KeyEventKind;
|
||||||
@@ -9,7 +9,7 @@ use ratatui::prelude::Widget;
|
|||||||
use ratatui::widgets::Clear;
|
use ratatui::widgets::Clear;
|
||||||
use ratatui::widgets::WidgetRef;
|
use ratatui::widgets::WidgetRef;
|
||||||
|
|
||||||
use codex_login::AuthMode;
|
use codex_protocol::mcp_protocol::AuthMode;
|
||||||
|
|
||||||
use crate::LoginStatus;
|
use crate::LoginStatus;
|
||||||
use crate::onboarding::auth::AuthModeWidget;
|
use crate::onboarding::auth::AuthModeWidget;
|
||||||
|
|||||||
Reference in New Issue
Block a user