codex: remove api provision helper binary

Drop the standalone helper binary and the CLI-only auth.json sync path from codex-login.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Michael Fan
2026-03-25 13:30:07 -04:00
parent b802f49ca2
commit a4d68acd12
4 changed files with 3 additions and 343 deletions

View File

@@ -1,23 +1,13 @@
//! Browser-based OAuth flow for provisioning OpenAI project API keys.
use std::fs::OpenOptions;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use codex_app_server_protocol::AuthMode;
use codex_client::build_reqwest_client_with_custom_ca;
use codex_utils_home_dir::find_codex_home;
use reqwest::Client;
use reqwest::Method;
use serde::Deserialize;
use serde::Serialize;
use url::Url;
use crate::auth::AuthDotJson;
use crate::pkce::PkceCodes;
use crate::server::AuthorizationCodeServer;
use crate::server::start_authorization_code_server;
@@ -147,240 +137,25 @@ pub fn start_api_provisioning(
})
}
pub async fn run_from_env() -> Result<(), HelperError> {
match parse_args(std::env::args())? {
ParseOutcome::Help(help) => {
println!("{help}");
Ok(())
}
ParseOutcome::Run(options) => {
let auth_path = resolve_codex_auth_path(options.codex_auth_path.as_deref())?;
let session = start_api_provisioning(options.api_provision_options())?;
session.open_browser_or_print();
let provisioned = session.finish().await?;
let codex_auth_synced = !options.skip_codex_auth_sync;
if codex_auth_synced {
sync_codex_api_key(&provisioned.project_api_key, &auth_path)?;
eprintln!("Synced project API key to {}.", auth_path.display());
} else {
eprintln!("Skipping Codex auth sync.");
}
let output = ScriptOutput {
sensitive_id: provisioned.sensitive_id,
organization_id: provisioned.organization_id,
organization_title: provisioned.organization_title,
default_project_id: provisioned.default_project_id,
default_project_title: provisioned.default_project_title,
project_api_key: provisioned.project_api_key,
codex_auth_path: auth_path.display().to_string(),
codex_auth_synced,
access_token: options
.include_access_token
.then_some(provisioned.access_token),
};
print_output(&output, options.output)?;
Ok(())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct CliOptions {
issuer: String,
client_id: String,
audience: String,
api_base: String,
app: String,
callback_port: u16,
scope: String,
api_key_name: String,
project_poll_interval_seconds: u64,
project_poll_timeout_seconds: u64,
codex_auth_path: Option<PathBuf>,
skip_codex_auth_sync: bool,
include_access_token: bool,
output: OutputFormat,
}
impl Default for CliOptions {
fn default() -> Self {
Self {
issuer: AUTH_ISSUER.to_string(),
client_id: PLATFORM_HYDRA_CLIENT_ID.to_string(),
audience: PLATFORM_AUDIENCE.to_string(),
api_base: DEFAULT_API_BASE.to_string(),
app: DEFAULT_APP.to_string(),
callback_port: DEFAULT_CALLBACK_PORT,
scope: DEFAULT_SCOPE.to_string(),
api_key_name: DEFAULT_PROJECT_API_KEY_NAME.to_string(),
project_poll_interval_seconds: DEFAULT_PROJECT_POLL_INTERVAL_SECONDS,
project_poll_timeout_seconds: DEFAULT_PROJECT_POLL_TIMEOUT_SECONDS,
codex_auth_path: None,
skip_codex_auth_sync: false,
include_access_token: false,
output: OutputFormat::Json,
}
}
}
impl CliOptions {
fn api_provision_options(&self) -> ApiProvisionOptions {
ApiProvisionOptions {
issuer: self.issuer.clone(),
client_id: self.client_id.clone(),
audience: self.audience.clone(),
api_base: self.api_base.clone(),
app: self.app.clone(),
callback_port: self.callback_port,
scope: self.scope.clone(),
api_key_name: self.api_key_name.clone(),
project_poll_interval_seconds: self.project_poll_interval_seconds,
project_poll_timeout_seconds: self.project_poll_timeout_seconds,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OutputFormat {
Json,
SensitiveId,
ApiKey,
}
impl OutputFormat {
fn parse(raw: &str) -> Result<Self, HelperError> {
match raw {
"json" => Ok(Self::Json),
"sensitive_id" => Ok(Self::SensitiveId),
"api_key" => Ok(Self::ApiKey),
_ => Err(HelperError::message(format!(
"invalid value for `--output`: `{raw}`"
))),
}
}
}
enum ParseOutcome {
Help(String),
Run(CliOptions),
}
fn parse_args<I>(args: I) -> Result<ParseOutcome, HelperError>
where
I: IntoIterator<Item = String>,
{
let mut args = args.into_iter();
let program = args
.next()
.unwrap_or_else(|| "get_sensitive_id_via_codex_oauth".to_string());
let mut options = CliOptions::default();
let mut rest = args.peekable();
while let Some(arg) = rest.next() {
match arg.as_str() {
"-h" | "--help" => return Ok(ParseOutcome::Help(usage(&program))),
"--issuer" => options.issuer = take_value(&mut rest, "--issuer")?,
"--client-id" => options.client_id = take_value(&mut rest, "--client-id")?,
"--audience" => options.audience = take_value(&mut rest, "--audience")?,
"--api-base" => options.api_base = take_value(&mut rest, "--api-base")?,
"--app" => options.app = take_value(&mut rest, "--app")?,
"--callback-port" => {
options.callback_port =
parse_u16(take_value(&mut rest, "--callback-port")?, "--callback-port")?
}
"--scope" => options.scope = take_value(&mut rest, "--scope")?,
"--api-key-name" => options.api_key_name = take_value(&mut rest, "--api-key-name")?,
"--project-poll-interval-seconds" => {
options.project_poll_interval_seconds = parse_u64(
take_value(&mut rest, "--project-poll-interval-seconds")?,
"--project-poll-interval-seconds",
)?
}
"--project-poll-timeout-seconds" => {
options.project_poll_timeout_seconds = parse_u64(
take_value(&mut rest, "--project-poll-timeout-seconds")?,
"--project-poll-timeout-seconds",
)?
}
"--codex-auth-path" => {
options.codex_auth_path =
Some(PathBuf::from(take_value(&mut rest, "--codex-auth-path")?))
}
"--skip-codex-auth-sync" => options.skip_codex_auth_sync = true,
"--include-access-token" => options.include_access_token = true,
"--output" => {
options.output = OutputFormat::parse(&take_value(&mut rest, "--output")?)?
}
_ => {
return Err(HelperError::message(format!(
"unknown argument `{arg}`\n\n{}",
usage(&program)
)));
}
}
}
validate_api_provision_options(&options.api_provision_options())?;
Ok(ParseOutcome::Run(options))
}
fn validate_api_provision_options(options: &ApiProvisionOptions) -> Result<(), HelperError> {
if options.project_poll_interval_seconds == 0 {
return Err(HelperError::message(
"--project-poll-interval-seconds must be greater than 0.".to_string(),
"project_poll_interval_seconds must be greater than 0.".to_string(),
));
}
if options.project_poll_timeout_seconds == 0 {
return Err(HelperError::message(
"--project-poll-timeout-seconds must be greater than 0.".to_string(),
"project_poll_timeout_seconds must be greater than 0.".to_string(),
));
}
if options.api_key_name.trim().is_empty() {
return Err(HelperError::message(
"--api-key-name must not be empty.".to_string(),
"api_key_name must not be empty.".to_string(),
));
}
Ok(())
}
fn usage(program: &str) -> String {
format!(
"Usage: {program} [OPTIONS]\n\n\
Options:\n\
--issuer URL OAuth issuer base URL\n\
--client-id ID Hydra client id to use\n\
--audience URL OAuth audience\n\
--api-base URL Base API URL for onboarding exchange\n\
--app NAME `app` value for /dashboard/onboarding/login\n\
--callback-port PORT Local callback port (default: {DEFAULT_CALLBACK_PORT})\n\
--scope SCOPE OAuth scope string\n\
--api-key-name NAME Provisioned project API key name\n\
--project-poll-interval-seconds SEC Delay between default-project checks\n\
--project-poll-timeout-seconds SEC Maximum wait for organization/project readiness\n\
--codex-auth-path PATH Explicit auth.json path\n\
--skip-codex-auth-sync Do not write the provisioned API key to auth.json\n\
--include-access-token Include the OAuth access token in JSON output\n\
--output FORMAT One of: json, sensitive_id, api_key\n\
-h, --help Show this help message"
)
}
fn take_value<I>(args: &mut std::iter::Peekable<I>, flag: &str) -> Result<String, HelperError>
where
I: Iterator<Item = String>,
{
args.next()
.ok_or_else(|| HelperError::message(format!("missing value for `{flag}`")))
}
fn parse_u16(raw: String, flag: &str) -> Result<u16, HelperError> {
raw.parse::<u16>()
.map_err(|err| HelperError::message(format!("invalid value for `{flag}`: {err}")))
}
fn parse_u64(raw: String, flag: &str) -> Result<u64, HelperError> {
raw.parse::<u64>()
.map_err(|err| HelperError::message(format!("invalid value for `{flag}`: {err}")))
}
fn build_authorize_url(
options: &ApiProvisionOptions,
redirect_uri: &str,
@@ -671,86 +446,6 @@ where
.map_err(|err| HelperError::message(format!("{url} returned invalid JSON: {err}")))
}
fn resolve_codex_auth_path(explicit: Option<&Path>) -> Result<PathBuf, HelperError> {
match explicit {
Some(path) => Ok(path.to_path_buf()),
None => Ok(find_codex_home()
.map_err(|err| HelperError::message(format!("failed to resolve CODEX_HOME: {err}")))?
.join("auth.json")),
}
}
fn sync_codex_api_key(api_key: &str, auth_path: &Path) -> Result<(), HelperError> {
if let Some(parent) = auth_path.parent() {
std::fs::create_dir_all(parent).map_err(|err| {
HelperError::message(format!(
"Failed to create auth directory {}: {err}",
parent.display()
))
})?;
}
let auth = AuthDotJson {
auth_mode: Some(AuthMode::ApiKey),
openai_api_key: Some(api_key.to_string()),
tokens: None,
last_refresh: None,
};
let json = format!(
"{}\n",
serde_json::to_string_pretty(&auth).map_err(|err| {
HelperError::message(format!("failed to serialize auth.json contents: {err}"))
})?
);
let mut options = OpenOptions::new();
options.truncate(true).write(true).create(true);
#[cfg(unix)]
{
options.mode(0o600);
}
let mut file = options.open(auth_path).map_err(|err| {
HelperError::message(format!(
"Failed to open {} for writing: {err}",
auth_path.display()
))
})?;
file.write_all(json.as_bytes()).map_err(|err| {
HelperError::message(format!("Failed to write {}: {err}", auth_path.display()))
})?;
file.flush().map_err(|err| {
HelperError::message(format!("Failed to flush {}: {err}", auth_path.display()))
})
}
fn print_output(output: &ScriptOutput, format: OutputFormat) -> Result<(), HelperError> {
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(output).map_err(|err| {
HelperError::message(format!("failed to serialize output: {err}"))
})?
);
}
OutputFormat::SensitiveId => println!("{}", output.sensitive_id),
OutputFormat::ApiKey => println!("{}", output.project_api_key),
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct ScriptOutput {
sensitive_id: String,
organization_id: String,
organization_title: Option<String>,
default_project_id: String,
default_project_title: Option<String>,
project_api_key: String,
codex_auth_path: String,
codex_auth_synced: bool,
#[serde(skip_serializing_if = "Option::is_none")]
access_token: Option<String>,
}
#[derive(Debug, Deserialize)]
struct OAuthTokens {
#[serde(rename = "id_token")]

View File

@@ -1,7 +1,6 @@
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::tempdir;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
@@ -58,26 +57,6 @@ fn find_default_project_returns_initial_project() {
assert_eq!(selected, projects.get(1));
}
#[test]
fn sync_codex_api_key_writes_expected_auth_json() {
let temp_dir = tempdir().expect("tempdir");
let auth_path = temp_dir.path().join("codex").join("auth.json");
sync_codex_api_key("sk-test-key", &auth_path).expect("sync auth");
let written = std::fs::read_to_string(&auth_path).expect("read auth");
let parsed: AuthDotJson = serde_json::from_str(&written).expect("parse auth");
assert_eq!(
parsed,
AuthDotJson {
auth_mode: Some(AuthMode::ApiKey),
openai_api_key: Some("sk-test-key".to_string()),
tokens: None,
last_refresh: None,
}
);
}
#[tokio::test]
async fn provision_from_authorization_code_provisions_api_key() {
let server = MockServer::start().await;

View File

@@ -1,12 +0,0 @@
#[tokio::main]
async fn main() {
if let Err(err) = codex_login::run_api_provision_helper_from_env().await {
eprintln!("{err}");
if let Some(body) = err.body()
&& !body.is_empty()
{
eprintln!("{body}");
}
std::process::exit(1);
}
}

View File

@@ -17,12 +17,10 @@ pub use server::ServerOptions;
pub use server::ShutdownHandle;
pub use server::run_login_server;
pub use ApiProvisionError as OnboardOauthHelperError;
pub use api_provision::ApiProvisionOptions;
pub use api_provision::HelperError as ApiProvisionError;
pub use api_provision::PendingApiProvisioning;
pub use api_provision::ProvisionedApiKey;
pub use api_provision::run_from_env as run_api_provision_helper_from_env;
pub use api_provision::start_api_provisioning;
pub use auth::AuthConfig;
pub use auth::AuthCredentialsStoreMode;