From 4a916ba914049b842acc0d81f4ca69ea89b3bd93 Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Fri, 8 Aug 2025 18:30:34 -0700 Subject: [PATCH] Show ChatGPT login URL during onboarding (#2028) ## Summary - display authentication URL in the ChatGPT sign-in screen while onboarding image --- README.md | 12 ++++++ codex-rs/login/src/lib.rs | 67 +++++++++++++++++++++-------- codex-rs/tui/src/onboarding/auth.rs | 36 ++++++++++++---- 3 files changed, 89 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 388777db..89063b7e 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - [Quickstart](#quickstart) - [Installing and running Codex CLI](#installing-and-running-codex-cli) - [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) - [Choosing Codex's level of autonomy](#choosing-codexs-level-of-autonomy) - [**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). +### 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 @ +``` + +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 If you prefer to pay-as-you-go, you can still authenticate with your OpenAI API key by setting it as an environment variable: diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index f25f885b..a1dad79e 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -9,6 +9,7 @@ use std::fs::OpenOptions; use std::fs::remove_file; use std::io::Read; use std::io::Write; +use std::io::{self}; #[cfg(unix)] use std::os::unix::fs::OpenOptionsExt; use std::path::Path; @@ -262,6 +263,50 @@ pub struct SpawnedLogin { pub stderr: Arc>>, } +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 { + 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>>, +} + +impl Write for AppendWriter { + fn write(&mut self, data: &[u8]) -> io::Result { + 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(mut reader: R, buf: Arc>>) { + 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. pub fn spawn_login_with_chatgpt(codex_home: &Path) -> std::io::Result { let script_path = write_login_script_to_disk()?; @@ -278,25 +323,11 @@ pub fn spawn_login_with_chatgpt(codex_home: &Path) -> std::io::Result, + login_child: Option, _frame_ticker: Option, } impl Drop for ContinueInBrowserState { 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() { // Best-effort terminate and reap the child to avoid zombies. let _ = locked.kill(); @@ -183,11 +183,31 @@ impl AuthModeWidget { let idx = self.current_frame(); let mut spans = vec![Span::from("> ")]; spans.extend(shimmer_spans("Finish signing in via your browser", idx)); - let lines = vec![ - Line::from(spans), - Line::from(""), + let mut lines = vec![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)), - ]; + ); Paragraph::new(lines) .wrap(Wrap { trim: false }) .render(area, buf); @@ -276,7 +296,7 @@ impl AuthModeWidget { self.spawn_completion_poller(child.clone()); self.sign_in_state = SignInState::ChatGptContinueInBrowser(ContinueInBrowserState { - _login_child: Some(child), + login_child: Some(child), _frame_ticker: Some(FrameTicker::new(self.event_tx.clone())), }); self.event_tx.send(AppEvent::RequestRedraw);