mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
[device-auth] Add device code auth as a standalone option when headless environment is detected. (#9333)
This commit is contained in:
@@ -14,11 +14,9 @@ use codex_cli::login::run_login_status;
|
||||
use codex_cli::login::run_login_with_api_key;
|
||||
use codex_cli::login::run_login_with_chatgpt;
|
||||
use codex_cli::login::run_login_with_device_code;
|
||||
use codex_cli::login::run_login_with_device_code_fallback_to_browser;
|
||||
use codex_cli::login::run_logout;
|
||||
use codex_cloud_tasks::Cli as CloudTasksCli;
|
||||
use codex_common::CliConfigOverrides;
|
||||
use codex_core::env::is_headless_environment;
|
||||
use codex_exec::Cli as ExecCli;
|
||||
use codex_exec::Command as ExecCommand;
|
||||
use codex_exec::ReviewArgs;
|
||||
@@ -600,13 +598,6 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
||||
} else if login_cli.with_api_key {
|
||||
let api_key = read_api_key_from_stdin();
|
||||
run_login_with_api_key(login_cli.config_overrides, api_key).await;
|
||||
} else if is_headless_environment() {
|
||||
run_login_with_device_code_fallback_to_browser(
|
||||
login_cli.config_overrides,
|
||||
login_cli.issuer_base_url,
|
||||
login_cli.client_id,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
run_login_with_chatgpt(login_cli.config_overrides).await;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use codex_core::auth::AuthCredentialsStoreMode;
|
||||
use codex_core::auth::CLIENT_ID;
|
||||
use codex_core::auth::login_with_api_key;
|
||||
use codex_core::auth::read_openai_api_key_from_env;
|
||||
use codex_core::env::is_headless_environment;
|
||||
use codex_login::DeviceCode;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::ShutdownHandle;
|
||||
@@ -59,6 +58,13 @@ pub(crate) enum SignInState {
|
||||
ApiKeyConfigured,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum SignInOption {
|
||||
ChatGpt,
|
||||
DeviceCode,
|
||||
ApiKey,
|
||||
}
|
||||
|
||||
const API_KEY_DISABLED_MESSAGE: &str = "API key login is disabled.";
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -96,42 +102,26 @@ impl KeyboardHandler for AuthModeWidget {
|
||||
|
||||
match key_event.code {
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
self.highlighted_mode = AuthMode::ChatGPT;
|
||||
}
|
||||
self.move_highlight(-1);
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
if self.is_api_login_allowed() {
|
||||
self.highlighted_mode = AuthMode::ApiKey;
|
||||
}
|
||||
self.move_highlight(1);
|
||||
}
|
||||
KeyCode::Char('1') => {
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
self.start_chatgpt_login();
|
||||
}
|
||||
self.select_option_by_index(0);
|
||||
}
|
||||
KeyCode::Char('2') => {
|
||||
if self.is_api_login_allowed() {
|
||||
self.start_api_key_entry();
|
||||
} else {
|
||||
self.disallow_api_login();
|
||||
}
|
||||
self.select_option_by_index(1);
|
||||
}
|
||||
KeyCode::Char('3') => {
|
||||
self.select_option_by_index(2);
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let sign_in_state = { (*self.sign_in_state.read().unwrap()).clone() };
|
||||
match sign_in_state {
|
||||
SignInState::PickMode => match self.highlighted_mode {
|
||||
AuthMode::ChatGPT if self.is_chatgpt_login_allowed() => {
|
||||
self.start_chatgpt_login();
|
||||
}
|
||||
AuthMode::ApiKey if self.is_api_login_allowed() => {
|
||||
self.start_api_key_entry();
|
||||
}
|
||||
AuthMode::ChatGPT => {}
|
||||
AuthMode::ApiKey => {
|
||||
self.disallow_api_login();
|
||||
}
|
||||
},
|
||||
SignInState::PickMode => {
|
||||
self.handle_sign_in_option(self.highlighted_mode);
|
||||
}
|
||||
SignInState::ChatGptSuccessMessage => {
|
||||
*self.sign_in_state.write().unwrap() = SignInState::ChatGptSuccess;
|
||||
}
|
||||
@@ -170,7 +160,7 @@ impl KeyboardHandler for AuthModeWidget {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AuthModeWidget {
|
||||
pub request_frame: FrameRequester,
|
||||
pub highlighted_mode: AuthMode,
|
||||
pub highlighted_mode: SignInOption,
|
||||
pub error: Option<String>,
|
||||
pub sign_in_state: Arc<RwLock<SignInState>>,
|
||||
pub codex_home: PathBuf,
|
||||
@@ -191,8 +181,75 @@ impl AuthModeWidget {
|
||||
!matches!(self.forced_login_method, Some(ForcedLoginMethod::Api))
|
||||
}
|
||||
|
||||
fn displayed_sign_in_options(&self) -> Vec<SignInOption> {
|
||||
let mut options = vec![SignInOption::ChatGpt];
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
options.push(SignInOption::DeviceCode);
|
||||
}
|
||||
if self.is_api_login_allowed() {
|
||||
options.push(SignInOption::ApiKey);
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
fn selectable_sign_in_options(&self) -> Vec<SignInOption> {
|
||||
let mut options = Vec::new();
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
options.push(SignInOption::ChatGpt);
|
||||
options.push(SignInOption::DeviceCode);
|
||||
}
|
||||
if self.is_api_login_allowed() {
|
||||
options.push(SignInOption::ApiKey);
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
fn move_highlight(&mut self, delta: isize) {
|
||||
let options = self.selectable_sign_in_options();
|
||||
if options.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_index = options
|
||||
.iter()
|
||||
.position(|option| *option == self.highlighted_mode)
|
||||
.unwrap_or(0);
|
||||
let next_index =
|
||||
(current_index as isize + delta).rem_euclid(options.len() as isize) as usize;
|
||||
self.highlighted_mode = options[next_index];
|
||||
}
|
||||
|
||||
fn select_option_by_index(&mut self, index: usize) {
|
||||
let options = self.displayed_sign_in_options();
|
||||
if let Some(option) = options.get(index).copied() {
|
||||
self.handle_sign_in_option(option);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_sign_in_option(&mut self, option: SignInOption) {
|
||||
match option {
|
||||
SignInOption::ChatGpt => {
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
self.start_chatgpt_login();
|
||||
}
|
||||
}
|
||||
SignInOption::DeviceCode => {
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
self.start_device_code_login();
|
||||
}
|
||||
}
|
||||
SignInOption::ApiKey => {
|
||||
if self.is_api_login_allowed() {
|
||||
self.start_api_key_entry();
|
||||
} else {
|
||||
self.disallow_api_login();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn disallow_api_login(&mut self) {
|
||||
self.highlighted_mode = AuthMode::ChatGPT;
|
||||
self.highlighted_mode = SignInOption::ChatGpt;
|
||||
self.error = Some(API_KEY_DISABLED_MESSAGE.to_string());
|
||||
*self.sign_in_state.write().unwrap() = SignInState::PickMode;
|
||||
self.request_frame.schedule_frame();
|
||||
@@ -212,7 +269,7 @@ impl AuthModeWidget {
|
||||
];
|
||||
|
||||
let create_mode_item = |idx: usize,
|
||||
selected_mode: AuthMode,
|
||||
selected_mode: SignInOption,
|
||||
text: &str,
|
||||
description: &str|
|
||||
-> Vec<Line<'static>> {
|
||||
@@ -221,11 +278,11 @@ impl AuthModeWidget {
|
||||
|
||||
let line1 = if is_selected {
|
||||
Line::from(vec![
|
||||
format!("{} {}. ", caret, idx + 1).cyan().dim(),
|
||||
format!("{caret} {index}. ", index = idx + 1).cyan().dim(),
|
||||
text.to_string().cyan(),
|
||||
])
|
||||
} else {
|
||||
format!(" {}. {text}", idx + 1).into()
|
||||
format!(" {index}. {text}", index = idx + 1).into()
|
||||
};
|
||||
|
||||
let line2 = if is_selected {
|
||||
@@ -242,27 +299,42 @@ impl AuthModeWidget {
|
||||
|
||||
let chatgpt_description = if !self.is_chatgpt_login_allowed() {
|
||||
"ChatGPT login is disabled"
|
||||
} else if is_headless_environment() {
|
||||
"Uses device code login (headless environment detected)"
|
||||
} else {
|
||||
"Usage included with Plus, Pro, Team, and Enterprise plans"
|
||||
};
|
||||
lines.extend(create_mode_item(
|
||||
0,
|
||||
AuthMode::ChatGPT,
|
||||
"Sign in with ChatGPT",
|
||||
chatgpt_description,
|
||||
));
|
||||
lines.push("".into());
|
||||
if self.is_api_login_allowed() {
|
||||
lines.extend(create_mode_item(
|
||||
1,
|
||||
AuthMode::ApiKey,
|
||||
"Provide your own API key",
|
||||
"Pay for what you use",
|
||||
));
|
||||
let device_code_description = "Sign in from another device with a one-time code";
|
||||
|
||||
for (idx, option) in self.displayed_sign_in_options().into_iter().enumerate() {
|
||||
match option {
|
||||
SignInOption::ChatGpt => {
|
||||
lines.extend(create_mode_item(
|
||||
idx,
|
||||
option,
|
||||
"Sign in with ChatGPT",
|
||||
chatgpt_description,
|
||||
));
|
||||
}
|
||||
SignInOption::DeviceCode => {
|
||||
lines.extend(create_mode_item(
|
||||
idx,
|
||||
option,
|
||||
"Sign in with Device Code",
|
||||
device_code_description,
|
||||
));
|
||||
}
|
||||
SignInOption::ApiKey => {
|
||||
lines.extend(create_mode_item(
|
||||
idx,
|
||||
option,
|
||||
"Provide your own API key",
|
||||
"Pay for what you use",
|
||||
));
|
||||
}
|
||||
}
|
||||
lines.push("".into());
|
||||
} else {
|
||||
}
|
||||
|
||||
if !self.is_api_login_allowed() {
|
||||
lines.push(
|
||||
" API key login is disabled by this workspace. Sign in with ChatGPT to continue."
|
||||
.dim()
|
||||
@@ -309,9 +381,9 @@ impl AuthModeWidget {
|
||||
]));
|
||||
lines.push("".into());
|
||||
lines.push(Line::from(vec![
|
||||
" On a remote or headless machine? Use ".into(),
|
||||
"codex login --device-auth".cyan(),
|
||||
" instead".into(),
|
||||
" On a remote or headless machine? Press Esc and choose ".into(),
|
||||
"Sign in with Device Code".cyan(),
|
||||
".".into(),
|
||||
]));
|
||||
lines.push("".into());
|
||||
}
|
||||
@@ -588,13 +660,21 @@ impl AuthModeWidget {
|
||||
self.request_frame.schedule_frame();
|
||||
}
|
||||
|
||||
fn handle_existing_chatgpt_login(&mut self) -> bool {
|
||||
if matches!(self.login_status, LoginStatus::AuthMode(AuthMode::ChatGPT)) {
|
||||
*self.sign_in_state.write().unwrap() = SignInState::ChatGptSuccess;
|
||||
self.request_frame.schedule_frame();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Kicks off the ChatGPT auth flow and keeps the UI state consistent with the attempt.
|
||||
fn start_chatgpt_login(&mut self) {
|
||||
// If we're already authenticated with ChatGPT, don't start a new login –
|
||||
// just proceed to the success message flow.
|
||||
if matches!(self.login_status, LoginStatus::AuthMode(AuthMode::ChatGPT)) {
|
||||
*self.sign_in_state.write().unwrap() = SignInState::ChatGptSuccess;
|
||||
self.request_frame.schedule_frame();
|
||||
if self.handle_existing_chatgpt_login() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -606,11 +686,6 @@ impl AuthModeWidget {
|
||||
self.cli_auth_credentials_store_mode,
|
||||
);
|
||||
|
||||
if is_headless_environment() {
|
||||
headless_chatgpt_login::start_headless_chatgpt_login(self, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
match run_login_server(opts) {
|
||||
Ok(child) => {
|
||||
let sign_in_state = self.sign_in_state.clone();
|
||||
@@ -650,6 +725,21 @@ impl AuthModeWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_device_code_login(&mut self) {
|
||||
if self.handle_existing_chatgpt_login() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.error = None;
|
||||
let opts = ServerOptions::new(
|
||||
self.codex_home.clone(),
|
||||
CLIENT_ID.to_string(),
|
||||
self.forced_chatgpt_workspace_id.clone(),
|
||||
self.cli_auth_credentials_store_mode,
|
||||
);
|
||||
headless_chatgpt_login::start_headless_chatgpt_login(self, opts);
|
||||
}
|
||||
}
|
||||
|
||||
impl StepStateProvider for AuthModeWidget {
|
||||
@@ -708,7 +798,7 @@ mod tests {
|
||||
let codex_home_path = codex_home.path().to_path_buf();
|
||||
let widget = AuthModeWidget {
|
||||
request_frame: FrameRequester::test_dummy(),
|
||||
highlighted_mode: AuthMode::ChatGPT,
|
||||
highlighted_mode: SignInOption::ChatGpt,
|
||||
error: None,
|
||||
sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)),
|
||||
codex_home: codex_home_path.clone(),
|
||||
|
||||
@@ -11,11 +11,11 @@ use ratatui::style::Color;
|
||||
use ratatui::widgets::Clear;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
|
||||
use crate::LoginStatus;
|
||||
use crate::onboarding::auth::AuthModeWidget;
|
||||
use crate::onboarding::auth::SignInOption;
|
||||
use crate::onboarding::auth::SignInState;
|
||||
use crate::onboarding::trust_directory::TrustDirectorySelection;
|
||||
use crate::onboarding::trust_directory::TrustDirectoryWidget;
|
||||
@@ -92,8 +92,8 @@ impl OnboardingScreen {
|
||||
)));
|
||||
if show_login_screen {
|
||||
let highlighted_mode = match forced_login_method {
|
||||
Some(ForcedLoginMethod::Api) => AuthMode::ApiKey,
|
||||
_ => AuthMode::ChatGPT,
|
||||
Some(ForcedLoginMethod::Api) => SignInOption::ApiKey,
|
||||
_ => SignInOption::ChatGpt,
|
||||
};
|
||||
steps.push(Step::Auth(AuthModeWidget {
|
||||
request_frame: tui.frame_requester(),
|
||||
|
||||
@@ -5,7 +5,6 @@ use codex_core::auth::AuthCredentialsStoreMode;
|
||||
use codex_core::auth::CLIENT_ID;
|
||||
use codex_core::auth::login_with_api_key;
|
||||
use codex_core::auth::read_openai_api_key_from_env;
|
||||
use codex_core::env::is_headless_environment;
|
||||
use codex_login::DeviceCode;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::ShutdownHandle;
|
||||
@@ -59,6 +58,13 @@ pub(crate) enum SignInState {
|
||||
ApiKeyConfigured,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum SignInOption {
|
||||
ChatGpt,
|
||||
DeviceCode,
|
||||
ApiKey,
|
||||
}
|
||||
|
||||
const API_KEY_DISABLED_MESSAGE: &str = "API key login is disabled.";
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -96,42 +102,26 @@ impl KeyboardHandler for AuthModeWidget {
|
||||
|
||||
match key_event.code {
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
self.highlighted_mode = AuthMode::ChatGPT;
|
||||
}
|
||||
self.move_highlight(-1);
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
if self.is_api_login_allowed() {
|
||||
self.highlighted_mode = AuthMode::ApiKey;
|
||||
}
|
||||
self.move_highlight(1);
|
||||
}
|
||||
KeyCode::Char('1') => {
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
self.start_chatgpt_login();
|
||||
}
|
||||
self.select_option_by_index(0);
|
||||
}
|
||||
KeyCode::Char('2') => {
|
||||
if self.is_api_login_allowed() {
|
||||
self.start_api_key_entry();
|
||||
} else {
|
||||
self.disallow_api_login();
|
||||
}
|
||||
self.select_option_by_index(1);
|
||||
}
|
||||
KeyCode::Char('3') => {
|
||||
self.select_option_by_index(2);
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let sign_in_state = { (*self.sign_in_state.read().unwrap()).clone() };
|
||||
match sign_in_state {
|
||||
SignInState::PickMode => match self.highlighted_mode {
|
||||
AuthMode::ChatGPT if self.is_chatgpt_login_allowed() => {
|
||||
self.start_chatgpt_login();
|
||||
}
|
||||
AuthMode::ApiKey if self.is_api_login_allowed() => {
|
||||
self.start_api_key_entry();
|
||||
}
|
||||
AuthMode::ChatGPT => {}
|
||||
AuthMode::ApiKey => {
|
||||
self.disallow_api_login();
|
||||
}
|
||||
},
|
||||
SignInState::PickMode => {
|
||||
self.handle_sign_in_option(self.highlighted_mode);
|
||||
}
|
||||
SignInState::ChatGptSuccessMessage => {
|
||||
*self.sign_in_state.write().unwrap() = SignInState::ChatGptSuccess;
|
||||
}
|
||||
@@ -170,7 +160,7 @@ impl KeyboardHandler for AuthModeWidget {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AuthModeWidget {
|
||||
pub request_frame: FrameRequester,
|
||||
pub highlighted_mode: AuthMode,
|
||||
pub highlighted_mode: SignInOption,
|
||||
pub error: Option<String>,
|
||||
pub sign_in_state: Arc<RwLock<SignInState>>,
|
||||
pub codex_home: PathBuf,
|
||||
@@ -191,8 +181,75 @@ impl AuthModeWidget {
|
||||
!matches!(self.forced_login_method, Some(ForcedLoginMethod::Api))
|
||||
}
|
||||
|
||||
fn displayed_sign_in_options(&self) -> Vec<SignInOption> {
|
||||
let mut options = vec![SignInOption::ChatGpt];
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
options.push(SignInOption::DeviceCode);
|
||||
}
|
||||
if self.is_api_login_allowed() {
|
||||
options.push(SignInOption::ApiKey);
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
fn selectable_sign_in_options(&self) -> Vec<SignInOption> {
|
||||
let mut options = Vec::new();
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
options.push(SignInOption::ChatGpt);
|
||||
options.push(SignInOption::DeviceCode);
|
||||
}
|
||||
if self.is_api_login_allowed() {
|
||||
options.push(SignInOption::ApiKey);
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
fn move_highlight(&mut self, delta: isize) {
|
||||
let options = self.selectable_sign_in_options();
|
||||
if options.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_index = options
|
||||
.iter()
|
||||
.position(|option| *option == self.highlighted_mode)
|
||||
.unwrap_or(0);
|
||||
let next_index =
|
||||
(current_index as isize + delta).rem_euclid(options.len() as isize) as usize;
|
||||
self.highlighted_mode = options[next_index];
|
||||
}
|
||||
|
||||
fn select_option_by_index(&mut self, index: usize) {
|
||||
let options = self.displayed_sign_in_options();
|
||||
if let Some(option) = options.get(index).copied() {
|
||||
self.handle_sign_in_option(option);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_sign_in_option(&mut self, option: SignInOption) {
|
||||
match option {
|
||||
SignInOption::ChatGpt => {
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
self.start_chatgpt_login();
|
||||
}
|
||||
}
|
||||
SignInOption::DeviceCode => {
|
||||
if self.is_chatgpt_login_allowed() {
|
||||
self.start_device_code_login();
|
||||
}
|
||||
}
|
||||
SignInOption::ApiKey => {
|
||||
if self.is_api_login_allowed() {
|
||||
self.start_api_key_entry();
|
||||
} else {
|
||||
self.disallow_api_login();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn disallow_api_login(&mut self) {
|
||||
self.highlighted_mode = AuthMode::ChatGPT;
|
||||
self.highlighted_mode = SignInOption::ChatGpt;
|
||||
self.error = Some(API_KEY_DISABLED_MESSAGE.to_string());
|
||||
*self.sign_in_state.write().unwrap() = SignInState::PickMode;
|
||||
self.request_frame.schedule_frame();
|
||||
@@ -212,7 +269,7 @@ impl AuthModeWidget {
|
||||
];
|
||||
|
||||
let create_mode_item = |idx: usize,
|
||||
selected_mode: AuthMode,
|
||||
selected_mode: SignInOption,
|
||||
text: &str,
|
||||
description: &str|
|
||||
-> Vec<Line<'static>> {
|
||||
@@ -221,11 +278,11 @@ impl AuthModeWidget {
|
||||
|
||||
let line1 = if is_selected {
|
||||
Line::from(vec![
|
||||
format!("{} {}. ", caret, idx + 1).cyan().dim(),
|
||||
format!("{caret} {index}. ", index = idx + 1).cyan().dim(),
|
||||
text.to_string().cyan(),
|
||||
])
|
||||
} else {
|
||||
format!(" {}. {text}", idx + 1).into()
|
||||
format!(" {index}. {text}", index = idx + 1).into()
|
||||
};
|
||||
|
||||
let line2 = if is_selected {
|
||||
@@ -242,27 +299,42 @@ impl AuthModeWidget {
|
||||
|
||||
let chatgpt_description = if !self.is_chatgpt_login_allowed() {
|
||||
"ChatGPT login is disabled"
|
||||
} else if is_headless_environment() {
|
||||
"Uses device code login (headless environment detected)"
|
||||
} else {
|
||||
"Usage included with Plus, Pro, Team, and Enterprise plans"
|
||||
};
|
||||
lines.extend(create_mode_item(
|
||||
0,
|
||||
AuthMode::ChatGPT,
|
||||
"Sign in with ChatGPT",
|
||||
chatgpt_description,
|
||||
));
|
||||
lines.push("".into());
|
||||
if self.is_api_login_allowed() {
|
||||
lines.extend(create_mode_item(
|
||||
1,
|
||||
AuthMode::ApiKey,
|
||||
"Provide your own API key",
|
||||
"Pay for what you use",
|
||||
));
|
||||
let device_code_description = "Sign in from another device with a one-time code";
|
||||
|
||||
for (idx, option) in self.displayed_sign_in_options().into_iter().enumerate() {
|
||||
match option {
|
||||
SignInOption::ChatGpt => {
|
||||
lines.extend(create_mode_item(
|
||||
idx,
|
||||
option,
|
||||
"Sign in with ChatGPT",
|
||||
chatgpt_description,
|
||||
));
|
||||
}
|
||||
SignInOption::DeviceCode => {
|
||||
lines.extend(create_mode_item(
|
||||
idx,
|
||||
option,
|
||||
"Sign in with Device Code",
|
||||
device_code_description,
|
||||
));
|
||||
}
|
||||
SignInOption::ApiKey => {
|
||||
lines.extend(create_mode_item(
|
||||
idx,
|
||||
option,
|
||||
"Provide your own API key",
|
||||
"Pay for what you use",
|
||||
));
|
||||
}
|
||||
}
|
||||
lines.push("".into());
|
||||
} else {
|
||||
}
|
||||
|
||||
if !self.is_api_login_allowed() {
|
||||
lines.push(
|
||||
" API key login is disabled by this workspace. Sign in with ChatGPT to continue."
|
||||
.dim()
|
||||
@@ -309,9 +381,9 @@ impl AuthModeWidget {
|
||||
]));
|
||||
lines.push("".into());
|
||||
lines.push(Line::from(vec![
|
||||
" On a remote or headless machine? Use ".into(),
|
||||
"codex login --device-auth".cyan(),
|
||||
" instead".into(),
|
||||
" On a remote or headless machine? Press Esc and choose ".into(),
|
||||
"Sign in with Device Code".cyan(),
|
||||
".".into(),
|
||||
]));
|
||||
lines.push("".into());
|
||||
}
|
||||
@@ -588,12 +660,20 @@ impl AuthModeWidget {
|
||||
self.request_frame.schedule_frame();
|
||||
}
|
||||
|
||||
fn start_chatgpt_login(&mut self) {
|
||||
// If we're already authenticated with ChatGPT, don't start a new login –
|
||||
// just proceed to the success message flow.
|
||||
fn handle_existing_chatgpt_login(&mut self) -> bool {
|
||||
if matches!(self.login_status, LoginStatus::AuthMode(AuthMode::ChatGPT)) {
|
||||
*self.sign_in_state.write().unwrap() = SignInState::ChatGptSuccess;
|
||||
self.request_frame.schedule_frame();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn start_chatgpt_login(&mut self) {
|
||||
// If we're already authenticated with ChatGPT, don't start a new login –
|
||||
// just proceed to the success message flow.
|
||||
if self.handle_existing_chatgpt_login() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -605,11 +685,6 @@ impl AuthModeWidget {
|
||||
self.cli_auth_credentials_store_mode,
|
||||
);
|
||||
|
||||
if is_headless_environment() {
|
||||
headless_chatgpt_login::start_headless_chatgpt_login(self, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
match run_login_server(opts) {
|
||||
Ok(child) => {
|
||||
let sign_in_state = self.sign_in_state.clone();
|
||||
@@ -649,6 +724,21 @@ impl AuthModeWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_device_code_login(&mut self) {
|
||||
if self.handle_existing_chatgpt_login() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.error = None;
|
||||
let opts = ServerOptions::new(
|
||||
self.codex_home.clone(),
|
||||
CLIENT_ID.to_string(),
|
||||
self.forced_chatgpt_workspace_id.clone(),
|
||||
self.cli_auth_credentials_store_mode,
|
||||
);
|
||||
headless_chatgpt_login::start_headless_chatgpt_login(self, opts);
|
||||
}
|
||||
}
|
||||
|
||||
impl StepStateProvider for AuthModeWidget {
|
||||
@@ -707,7 +797,7 @@ mod tests {
|
||||
let codex_home_path = codex_home.path().to_path_buf();
|
||||
let widget = AuthModeWidget {
|
||||
request_frame: FrameRequester::test_dummy(),
|
||||
highlighted_mode: AuthMode::ChatGPT,
|
||||
highlighted_mode: SignInOption::ChatGpt,
|
||||
error: None,
|
||||
sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)),
|
||||
codex_home: codex_home_path.clone(),
|
||||
|
||||
@@ -11,11 +11,11 @@ use ratatui::style::Color;
|
||||
use ratatui::widgets::Clear;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
|
||||
use crate::LoginStatus;
|
||||
use crate::onboarding::auth::AuthModeWidget;
|
||||
use crate::onboarding::auth::SignInOption;
|
||||
use crate::onboarding::auth::SignInState;
|
||||
use crate::onboarding::trust_directory::TrustDirectorySelection;
|
||||
use crate::onboarding::trust_directory::TrustDirectoryWidget;
|
||||
@@ -92,8 +92,8 @@ impl OnboardingScreen {
|
||||
)));
|
||||
if show_login_screen {
|
||||
let highlighted_mode = match forced_login_method {
|
||||
Some(ForcedLoginMethod::Api) => AuthMode::ApiKey,
|
||||
_ => AuthMode::ChatGPT,
|
||||
Some(ForcedLoginMethod::Api) => SignInOption::ApiKey,
|
||||
_ => SignInOption::ChatGpt,
|
||||
};
|
||||
steps.push(Step::Auth(AuthModeWidget {
|
||||
request_frame: tui.frame_requester(),
|
||||
|
||||
Reference in New Issue
Block a user