Show ChatGPT login URL during onboarding (#2028)
## Summary - display authentication URL in the ChatGPT sign-in screen while onboarding <img width="684" height="151" alt="image" src="https://github.com/user-attachments/assets/a8c32cb0-77f6-4a3f-ae3b-6695247c994d" />
This commit is contained in:
12
README.md
12
README.md
@@ -18,6 +18,7 @@
|
|||||||
- [Quickstart](#quickstart)
|
- [Quickstart](#quickstart)
|
||||||
- [Installing and running Codex CLI](#installing-and-running-codex-cli)
|
- [Installing and running Codex CLI](#installing-and-running-codex-cli)
|
||||||
- [Using Codex with your ChatGPT plan](#using-codex-with-your-chatgpt-plan)
|
- [Using Codex with your ChatGPT plan](#using-codex-with-your-chatgpt-plan)
|
||||||
|
- [Connecting through VPS or remote](#connecting-through-vps-or-remote)
|
||||||
- [Usage-based billing alternative: Use an OpenAI API key](#usage-based-billing-alternative-use-an-openai-api-key)
|
- [Usage-based billing alternative: Use an OpenAI API key](#usage-based-billing-alternative-use-an-openai-api-key)
|
||||||
- [Choosing Codex's level of autonomy](#choosing-codexs-level-of-autonomy)
|
- [Choosing Codex's level of autonomy](#choosing-codexs-level-of-autonomy)
|
||||||
- [**1. Read/write**](#1-readwrite)
|
- [**1. Read/write**](#1-readwrite)
|
||||||
@@ -108,6 +109,17 @@ After you run `codex` select Sign in with ChatGPT. You'll need a Plus, Pro, or T
|
|||||||
|
|
||||||
If you encounter problems with the login flow, please comment on [this issue](https://github.com/openai/codex/issues/1243).
|
If you encounter problems with the login flow, please comment on [this issue](https://github.com/openai/codex/issues/1243).
|
||||||
|
|
||||||
|
### Connecting through VPS or remote
|
||||||
|
|
||||||
|
If you run Codex on a remote machine (VPS/server) without a local browser, the login helper starts a server on `localhost:1455` on the remote host. To complete login in your local browser, forward that port to your machine before starting the login flow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From your local machine
|
||||||
|
ssh -L 1455:localhost:1455 <user>@<remote-host>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in that SSH session, run `codex` and select "Sign in with ChatGPT". When prompted, open the printed URL (it will be `http://localhost:1455/...`) in your local browser. The traffic will be tunneled to the remote server.
|
||||||
|
|
||||||
### Usage-based billing alternative: Use an OpenAI API key
|
### Usage-based billing alternative: Use an OpenAI API key
|
||||||
|
|
||||||
If you prefer to pay-as-you-go, you can still authenticate with your OpenAI API key by setting it as an environment variable:
|
If you prefer to pay-as-you-go, you can still authenticate with your OpenAI API key by setting it as an environment variable:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use std::fs::OpenOptions;
|
|||||||
use std::fs::remove_file;
|
use std::fs::remove_file;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::io::{self};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -262,6 +263,50 @@ pub struct SpawnedLogin {
|
|||||||
pub stderr: Arc<Mutex<Vec<u8>>>,
|
pub stderr: Arc<Mutex<Vec<u8>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SpawnedLogin {
|
||||||
|
/// Returns the login URL, if one has been emitted by the login subprocess.
|
||||||
|
///
|
||||||
|
/// The Python helper prints the URL to stderr; we capture it and extract
|
||||||
|
/// the last whitespace-separated token that starts with "http".
|
||||||
|
pub fn get_login_url(&self) -> Option<String> {
|
||||||
|
self.stderr
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|buffer| String::from_utf8(buffer.clone()).ok())
|
||||||
|
.and_then(|output| {
|
||||||
|
output
|
||||||
|
.split_whitespace()
|
||||||
|
.filter(|part| part.starts_with("http"))
|
||||||
|
.next_back()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers for streaming child output into shared buffers
|
||||||
|
struct AppendWriter {
|
||||||
|
buf: Arc<Mutex<Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for AppendWriter {
|
||||||
|
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
|
||||||
|
if let Ok(mut b) = self.buf.lock() {
|
||||||
|
b.extend_from_slice(data);
|
||||||
|
}
|
||||||
|
Ok(data.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_pipe_reader<R: Read + Send + 'static>(mut reader: R, buf: Arc<Mutex<Vec<u8>>>) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = io::copy(&mut reader, &mut AppendWriter { buf });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn the ChatGPT login Python server as a child process and return a handle to its process.
|
/// Spawn the ChatGPT login Python server as a child process and return a handle to its process.
|
||||||
pub fn spawn_login_with_chatgpt(codex_home: &Path) -> std::io::Result<SpawnedLogin> {
|
pub fn spawn_login_with_chatgpt(codex_home: &Path) -> std::io::Result<SpawnedLogin> {
|
||||||
let script_path = write_login_script_to_disk()?;
|
let script_path = write_login_script_to_disk()?;
|
||||||
@@ -278,25 +323,11 @@ pub fn spawn_login_with_chatgpt(codex_home: &Path) -> std::io::Result<SpawnedLog
|
|||||||
let stdout_buf = Arc::new(Mutex::new(Vec::new()));
|
let stdout_buf = Arc::new(Mutex::new(Vec::new()));
|
||||||
let stderr_buf = Arc::new(Mutex::new(Vec::new()));
|
let stderr_buf = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
||||||
if let Some(mut out) = child.stdout.take() {
|
if let Some(out) = child.stdout.take() {
|
||||||
let buf = stdout_buf.clone();
|
spawn_pipe_reader(out, stdout_buf.clone());
|
||||||
std::thread::spawn(move || {
|
|
||||||
let mut tmp = Vec::new();
|
|
||||||
let _ = std::io::copy(&mut out, &mut tmp);
|
|
||||||
if let Ok(mut b) = buf.lock() {
|
|
||||||
b.extend_from_slice(&tmp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if let Some(mut err) = child.stderr.take() {
|
if let Some(err) = child.stderr.take() {
|
||||||
let buf = stderr_buf.clone();
|
spawn_pipe_reader(err, stderr_buf.clone());
|
||||||
std::thread::spawn(move || {
|
|
||||||
let mut tmp = Vec::new();
|
|
||||||
let _ = std::io::copy(&mut err, &mut tmp);
|
|
||||||
if let Ok(mut b) = buf.lock() {
|
|
||||||
b.extend_from_slice(&tmp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SpawnedLogin {
|
Ok(SpawnedLogin {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ use super::onboarding_screen::StepState;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum SignInState {
|
pub(crate) enum SignInState {
|
||||||
PickMode,
|
PickMode,
|
||||||
ChatGptContinueInBrowser(#[allow(dead_code)] ContinueInBrowserState),
|
ChatGptContinueInBrowser(ContinueInBrowserState),
|
||||||
ChatGptSuccessMessage,
|
ChatGptSuccessMessage,
|
||||||
ChatGptSuccess,
|
ChatGptSuccess,
|
||||||
EnvVarMissing,
|
EnvVarMissing,
|
||||||
@@ -40,12 +40,12 @@ pub(crate) enum SignInState {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
/// Used to manage the lifecycle of SpawnedLogin and FrameTicker and ensure they get cleaned up.
|
/// Used to manage the lifecycle of SpawnedLogin and FrameTicker and ensure they get cleaned up.
|
||||||
pub(crate) struct ContinueInBrowserState {
|
pub(crate) struct ContinueInBrowserState {
|
||||||
_login_child: Option<codex_login::SpawnedLogin>,
|
login_child: Option<codex_login::SpawnedLogin>,
|
||||||
_frame_ticker: Option<FrameTicker>,
|
_frame_ticker: Option<FrameTicker>,
|
||||||
}
|
}
|
||||||
impl Drop for ContinueInBrowserState {
|
impl Drop for ContinueInBrowserState {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(child) = &self._login_child {
|
if let Some(child) = &self.login_child {
|
||||||
if let Ok(mut locked) = child.child.lock() {
|
if let Ok(mut locked) = child.child.lock() {
|
||||||
// Best-effort terminate and reap the child to avoid zombies.
|
// Best-effort terminate and reap the child to avoid zombies.
|
||||||
let _ = locked.kill();
|
let _ = locked.kill();
|
||||||
@@ -183,11 +183,31 @@ impl AuthModeWidget {
|
|||||||
let idx = self.current_frame();
|
let idx = self.current_frame();
|
||||||
let mut spans = vec![Span::from("> ")];
|
let mut spans = vec![Span::from("> ")];
|
||||||
spans.extend(shimmer_spans("Finish signing in via your browser", idx));
|
spans.extend(shimmer_spans("Finish signing in via your browser", idx));
|
||||||
let lines = vec![
|
let mut lines = vec![Line::from(spans), Line::from("")];
|
||||||
Line::from(spans),
|
|
||||||
Line::from(""),
|
if let SignInState::ChatGptContinueInBrowser(state) = &self.sign_in_state {
|
||||||
|
if let Some(url) = state
|
||||||
|
.login_child
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|child| child.get_login_url())
|
||||||
|
{
|
||||||
|
lines.push(Line::from(" If the link doesn't open automatically, open the following link to authenticate:"));
|
||||||
|
lines.push(Line::from(vec![
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::styled(
|
||||||
|
url,
|
||||||
|
Style::default()
|
||||||
|
.fg(LIGHT_BLUE)
|
||||||
|
.add_modifier(Modifier::UNDERLINED),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
Line::from(" Press Esc to cancel").style(Style::default().add_modifier(Modifier::DIM)),
|
Line::from(" Press Esc to cancel").style(Style::default().add_modifier(Modifier::DIM)),
|
||||||
];
|
);
|
||||||
Paragraph::new(lines)
|
Paragraph::new(lines)
|
||||||
.wrap(Wrap { trim: false })
|
.wrap(Wrap { trim: false })
|
||||||
.render(area, buf);
|
.render(area, buf);
|
||||||
@@ -276,7 +296,7 @@ impl AuthModeWidget {
|
|||||||
self.spawn_completion_poller(child.clone());
|
self.spawn_completion_poller(child.clone());
|
||||||
self.sign_in_state =
|
self.sign_in_state =
|
||||||
SignInState::ChatGptContinueInBrowser(ContinueInBrowserState {
|
SignInState::ChatGptContinueInBrowser(ContinueInBrowserState {
|
||||||
_login_child: Some(child),
|
login_child: Some(child),
|
||||||
_frame_ticker: Some(FrameTicker::new(self.event_tx.clone())),
|
_frame_ticker: Some(FrameTicker::new(self.event_tx.clone())),
|
||||||
});
|
});
|
||||||
self.event_tx.send(AppEvent::RequestRedraw);
|
self.event_tx.send(AppEvent::RequestRedraw);
|
||||||
|
|||||||
Reference in New Issue
Block a user