Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 77 additions & 18 deletions src/codex_agent.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use acp::schema::{
AgentAuthCapabilities, AgentCapabilities, AuthEnvVar, AuthMethod, AuthMethodAgent,
AuthMethodEnvVar, AuthMethodId, AuthenticateRequest, AuthenticateResponse, CancelNotification,
ClientCapabilities, CloseSessionRequest, CloseSessionResponse, Implementation,
InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse,
LoadSessionRequest, LoadSessionResponse, LogoutCapabilities, LogoutRequest, LogoutResponse,
McpCapabilities, McpServer, McpServerHttp, McpServerStdio, NewSessionRequest,
NewSessionResponse, PromptCapabilities, PromptRequest, PromptResponse, ProtocolVersion,
SessionCapabilities, SessionCloseCapabilities, SessionId, SessionInfo, SessionListCapabilities,
SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest,
SetSessionModeResponse, SetSessionModelRequest, SetSessionModelResponse,
AuthMethodEnvVar, AuthMethodId, AuthMethodTerminal, AuthenticateRequest, AuthenticateResponse,
CancelNotification, ClientCapabilities, CloseSessionRequest, CloseSessionResponse,
Implementation, InitializeRequest, InitializeResponse, ListSessionsRequest,
ListSessionsResponse, LoadSessionRequest, LoadSessionResponse, LogoutCapabilities,
LogoutRequest, LogoutResponse, McpCapabilities, McpServer, McpServerHttp, McpServerStdio,
NewSessionRequest, NewSessionResponse, PromptCapabilities, PromptRequest, PromptResponse,
ProtocolVersion, SessionCapabilities, SessionCloseCapabilities, SessionId, SessionInfo,
SessionListCapabilities, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse,
SetSessionModeRequest, SetSessionModeResponse, SetSessionModelRequest, SetSessionModelResponse,
};
use acp::{Agent, Client, ConnectTo, ConnectionTo, Error};
use agent_client_protocol as acp;
Expand All @@ -34,6 +34,7 @@ use std::{
use tracing::{debug, info};
use unicode_segmentation::UnicodeSegmentation;

use crate::run_device_code_auth;
use crate::thread::Thread;

/// The Codex implementation of the ACP Agent.
Expand Down Expand Up @@ -434,15 +435,11 @@ impl CodexAgent {
.close(SessionCloseCapabilities::new())
.list(SessionListCapabilities::new());

let mut auth_methods = vec![
CodexAuthMethod::ChatGpt.into(),
let auth_methods = vec![
chatgpt_auth_method(no_browser_mode()),
CodexAuthMethod::CodexApiKey.into(),
CodexAuthMethod::OpenAiApiKey.into(),
];
// Until codex device code auth works, we can't use this in remote ssh projects
if std::env::var("NO_BROWSER").is_ok() {
auth_methods.remove(0);
}

Ok(InitializeResponse::new(protocol_version)
.agent_capabilities(agent_capabilities)
Expand Down Expand Up @@ -472,17 +469,28 @@ impl CodexAgent {

match auth_method {
CodexAuthMethod::ChatGpt => {
// Perform browser/device login via codex-rs, then report success/failure to the client.
let opts = codex_login::ServerOptions::new(
self.config.codex_home.to_path_buf(),
codex_login::auth::CLIENT_ID.to_string(),
None,
self.config.cli_auth_credentials_store_mode,
);

let server =
codex_login::run_login_server(opts).map_err(Error::into_internal_error)?;
if no_browser_mode() {
run_device_code_auth(opts)
.await
.map_err(Error::into_internal_error)?;
} else {
let server =
codex_login::run_login_server(opts).map_err(Error::into_internal_error)?;

server
.block_until_done()
.await
.map_err(Error::into_internal_error)?;

self.auth_manager.reload();
}
server
.block_until_done()
.await
Expand Down Expand Up @@ -800,6 +808,28 @@ impl CodexAgent {
}
}

fn no_browser_mode() -> bool {
std::env::var_os("NO_BROWSER").is_some()
}

fn chatgpt_auth_method(no_browser: bool) -> AuthMethod {
let method = CodexAuthMethod::ChatGpt;
if no_browser {
let description = "Use your ChatGPT login with Codex CLI via device code (works in remote/headless projects)";
AuthMethod::Terminal(
AuthMethodTerminal::new(method, "Login with ChatGPT")
.description(description)
.args(vec!["/auth".to_string()])
.env(HashMap::from_iter([(
"NO_BROWSER".to_string(),
"1".to_string(),
)])),
)
} else {
method.into()
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CodexAuthMethod {
ChatGpt,
Expand Down Expand Up @@ -892,3 +922,32 @@ fn format_session_title(message: &str) -> Option<String> {
Some(truncate_graphemes(trimmed, SESSION_TITLE_MAX_GRAPHEMES))
}
}

#[cfg(test)]
mod tests {
use super::{AuthMethod, AuthMethodId, chatgpt_auth_method};

#[test]
fn headless_chatgpt_auth_uses_terminal_auth() {
match chatgpt_auth_method(true) {
AuthMethod::Terminal(method) => {
assert_eq!(method.id, AuthMethodId::new("chatgpt"));
assert_eq!(method.name, "Login with ChatGPT");
assert_eq!(method.args, vec!["/auth"]);
assert_eq!(method.env.get("NO_BROWSER").map(String::as_str), Some("1"));
}
other => panic!("expected terminal auth method, got {other:?}"),
}
}

#[test]
fn interactive_chatgpt_auth_uses_agent_auth() {
match chatgpt_auth_method(false) {
AuthMethod::Agent(method) => {
assert_eq!(method.id, AuthMethodId::new("chatgpt"));
assert_eq!(method.name, "Login with ChatGPT");
}
other => panic!("expected agent auth method, got {other:?}"),
}
}
}
68 changes: 55 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use agent_client_protocol::ByteStreams;
use codex_core::config::{Config, ConfigOverrides};
use codex_utils_cli::CliConfigOverrides;
use std::io::Write as _;
use std::path::PathBuf;
use std::sync::Arc;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
Expand Down Expand Up @@ -31,7 +32,38 @@ pub async fn run_main(
.with_env_filter(EnvFilter::from_default_env())
.init();

// Parse CLI overrides and load configuration
let config = load_config(cli_config_overrides, codex_linux_sandbox_exe.clone()).await?;

let agent = Arc::new(codex_agent::CodexAgent::new(config, codex_linux_sandbox_exe).await?);

let stdin = tokio::io::stdin().compat();
let stdout = tokio::io::stdout().compat_write();

agent
.serve(ByteStreams::new(stdout, stdin))
.await
.map_err(|e| std::io::Error::other(format!("ACP error: {e}")))?;

Ok(())
}

pub async fn run_auth_command(cli_config_overrides: CliConfigOverrides) -> std::io::Result<()> {
let config = load_config(cli_config_overrides, None).await?;

let opts = codex_login::ServerOptions::new(
config.codex_home.to_path_buf(),
codex_login::auth::CLIENT_ID.to_string(),
None,
config.cli_auth_credentials_store_mode,
);

run_device_code_auth(opts).await
}

async fn load_config(
cli_config_overrides: CliConfigOverrides,
codex_linux_sandbox_exe: Option<PathBuf>,
) -> std::io::Result<Config> {
let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
Expand All @@ -40,7 +72,7 @@ pub async fn run_main(
})?;

let config_overrides = ConfigOverrides {
codex_linux_sandbox_exe: codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe,
..ConfigOverrides::default()
};

Expand All @@ -53,23 +85,33 @@ pub async fn run_main(
format!("error loading config: {e}"),
)
})?;
// Apply residency requirement so the HTTP client sends the
// x-openai-internal-codex-residency header on all requests.

codex_login::default_client::set_default_client_residency_requirement(
config.enforce_residency.value(),
);

let agent = Arc::new(codex_agent::CodexAgent::new(config, codex_linux_sandbox_exe).await?);

let stdin = tokio::io::stdin().compat();
let stdout = tokio::io::stdout().compat_write();
Ok(config)
}

agent
.serve(ByteStreams::new(stdout, stdin))
.await
.map_err(|e| std::io::Error::other(format!("ACP error: {e}")))?;
pub(crate) async fn run_device_code_auth(opts: codex_login::ServerOptions) -> std::io::Result<()> {
let device_code = codex_login::request_device_code(&opts).await?;
{
let mut stderr = std::io::stderr().lock();
writeln!(
stderr,
"Open this link in your browser and sign in to your ChatGPT account:\n{}\n",
device_code.verification_url
)?;
writeln!(
stderr,
"Then enter this one-time code (expires in 15 minutes):\n{}\n",
device_code.user_code
)?;
writeln!(stderr, "Waiting for login to complete in the browser...")?;
stderr.flush()?;
}

Ok(())
codex_login::complete_device_code_login(opts, device_code).await
}

// Re-export the MCP server types for compatibility
Expand Down
82 changes: 80 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,89 @@ use anyhow::Result;
use clap::Parser;
use codex_arg0::arg0_dispatch_or_else;
use codex_utils_cli::CliConfigOverrides;
use std::ffi::OsString;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CommandMode {
Acp,
Auth,
}

fn command_mode_and_args_from(mut args: Vec<OsString>) -> (CommandMode, Vec<OsString>) {
let mode = if matches!(args.get(1), Some(arg) if arg == "/auth") {
args.remove(1);
CommandMode::Auth
} else {
CommandMode::Acp
};

(mode, args)
}

fn command_mode_and_args() -> (CommandMode, Vec<OsString>) {
command_mode_and_args_from(std::env::args_os().collect())
}

fn main() -> Result<()> {
let (command_mode, filtered_args) = command_mode_and_args();

arg0_dispatch_or_else(|args| async move {
let cli_config_overrides = CliConfigOverrides::parse();
codex_acp::run_main(args.codex_linux_sandbox_exe, cli_config_overrides).await?;
let cli_config_overrides = CliConfigOverrides::parse_from(filtered_args);

match command_mode {
CommandMode::Acp => {
codex_acp::run_main(args.codex_linux_sandbox_exe, cli_config_overrides).await?;
}
CommandMode::Auth => {
codex_acp::run_auth_command(cli_config_overrides).await?;
}
}

Ok(())
})
}

#[cfg(test)]
mod tests {
use super::{CommandMode, command_mode_and_args_from};
use std::ffi::OsString;

#[test]
fn auth_mode_only_matches_first_positional_argument() {
let (mode, args) = command_mode_and_args_from(vec![
OsString::from("codex-acp"),
OsString::from("--verbose"),
OsString::from("/auth"),
]);

assert_eq!(mode, CommandMode::Acp);
assert_eq!(
args,
vec![
OsString::from("codex-acp"),
OsString::from("--verbose"),
OsString::from("/auth"),
]
);
}

#[test]
fn auth_mode_strips_explicit_subcommand() {
let (mode, args) = command_mode_and_args_from(vec![
OsString::from("codex-acp"),
OsString::from("/auth"),
OsString::from("--config"),
OsString::from("settings.toml"),
]);

assert_eq!(mode, CommandMode::Auth);
assert_eq!(
args,
vec![
OsString::from("codex-acp"),
OsString::from("--config"),
OsString::from("settings.toml"),
]
);
}
}