codex: rename api-provision slash command

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Michael Fan
2026-03-26 16:57:49 -04:00
co-authored by Codex
parent cb37b3e641
commit 285c4926b5
11 changed files with 98 additions and 98 deletions
@@ -1,4 +1,4 @@
//! Browser-based OAuth flow for provisioning OpenAI project API keys.
//! Browser-based OAuth flow for creating OpenAI project API keys.
use std::time::Duration;
@@ -20,7 +20,7 @@ const CALLBACK_PORT: u16 = 0;
const CALLBACK_PATH: &str = "/auth/callback";
const SCOPE: &str = "openid email profile offline_access";
const APP: &str = "api";
const USER_AGENT: &str = "Codex-API-Provision/1.0";
const USER_AGENT: &str = "Codex-Create-API-Key/1.0";
const PROJECT_API_KEY_NAME: &str = "Codex CLI";
const PROJECT_POLL_INTERVAL_SECONDS: u64 = 10;
const PROJECT_POLL_TIMEOUT_SECONDS: u64 = 60;
@@ -28,7 +28,7 @@ const OAUTH_TIMEOUT_SECONDS: u64 = 15 * 60;
const HTTP_TIMEOUT_SECONDS: u64 = 30;
#[derive(Debug, Clone, PartialEq, Eq)]
struct ApiProvisionOptions {
struct CreateApiKeyOptions {
issuer: String,
client_id: String,
audience: String,
@@ -41,15 +41,15 @@ struct ApiProvisionOptions {
project_poll_timeout_seconds: u64,
}
pub struct PendingApiProvisioning {
pub struct PendingCreateApiKey {
client: Client,
options: ApiProvisionOptions,
options: CreateApiKeyOptions,
redirect_uri: String,
code_verifier: String,
callback_server: AuthorizationCodeServer,
}
impl PendingApiProvisioning {
impl PendingCreateApiKey {
pub fn auth_url(&self) -> &str {
&self.callback_server.auth_url
}
@@ -62,13 +62,13 @@ impl PendingApiProvisioning {
self.callback_server.open_browser()
}
pub async fn finish(self) -> Result<ProvisionedApiKey, ApiProvisionError> {
pub async fn finish(self) -> Result<CreatedApiKey, CreateApiKeyError> {
let code = self
.callback_server
.wait_for_code(Duration::from_secs(OAUTH_TIMEOUT_SECONDS))
.await
.map_err(|err| ApiProvisionError::message(err.to_string()))?;
provision_from_authorization_code(
.map_err(|err| CreateApiKeyError::message(err.to_string()))?;
create_api_key_from_authorization_code(
&self.client,
&self.options,
&self.redirect_uri,
@@ -80,7 +80,7 @@ impl PendingApiProvisioning {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProvisionedApiKey {
pub struct CreatedApiKey {
pub organization_id: String,
pub organization_title: Option<String>,
pub default_project_id: String,
@@ -88,8 +88,8 @@ pub struct ProvisionedApiKey {
pub project_api_key: String,
}
pub fn start_api_provisioning() -> Result<PendingApiProvisioning, ApiProvisionError> {
let options = ApiProvisionOptions {
pub fn start_create_api_key() -> Result<PendingCreateApiKey, CreateApiKeyError> {
let options = CreateApiKeyOptions {
issuer: AUTH_ISSUER.to_string(),
client_id: PLATFORM_HYDRA_CLIENT_ID.to_string(),
audience: PLATFORM_AUDIENCE.to_string(),
@@ -111,10 +111,10 @@ pub fn start_api_provisioning() -> Result<PendingApiProvisioning, ApiProvisionEr
.map_err(|err| std::io::Error::other(err.to_string()))
},
)
.map_err(|err| ApiProvisionError::message(err.to_string()))?;
.map_err(|err| CreateApiKeyError::message(err.to_string()))?;
let redirect_uri = callback_server.redirect_uri.clone();
Ok(PendingApiProvisioning {
Ok(PendingCreateApiKey {
client,
options,
redirect_uri,
@@ -124,16 +124,16 @@ pub fn start_api_provisioning() -> Result<PendingApiProvisioning, ApiProvisionEr
}
fn build_authorize_url(
options: &ApiProvisionOptions,
options: &CreateApiKeyOptions,
redirect_uri: &str,
pkce: &PkceCodes,
state: &str,
) -> Result<String, ApiProvisionError> {
) -> Result<String, CreateApiKeyError> {
let mut url = Url::parse(&format!(
"{}/oauth/authorize",
options.issuer.trim_end_matches('/')
))
.map_err(|err| ApiProvisionError::message(format!("invalid issuer URL: {err}")))?;
.map_err(|err| CreateApiKeyError::message(format!("invalid issuer URL: {err}")))?;
url.query_pairs_mut()
.append_pair("audience", &options.audience)
.append_pair("client_id", &options.client_id)
@@ -146,20 +146,20 @@ fn build_authorize_url(
Ok(url.to_string())
}
fn build_http_client() -> Result<Client, ApiProvisionError> {
fn build_http_client() -> Result<Client, CreateApiKeyError> {
build_reqwest_client_with_custom_ca(
reqwest::Client::builder().timeout(Duration::from_secs(HTTP_TIMEOUT_SECONDS)),
)
.map_err(|err| ApiProvisionError::message(format!("failed to build HTTP client: {err}")))
.map_err(|err| CreateApiKeyError::message(format!("failed to build HTTP client: {err}")))
}
async fn provision_from_authorization_code(
async fn create_api_key_from_authorization_code(
client: &Client,
options: &ApiProvisionOptions,
options: &CreateApiKeyOptions,
redirect_uri: &str,
code_verifier: &str,
code: &str,
) -> Result<ProvisionedApiKey, ApiProvisionError> {
) -> Result<CreatedApiKey, CreateApiKeyError> {
let tokens = exchange_authorization_code_for_tokens(
client,
&options.issuer,
@@ -195,7 +195,7 @@ async fn provision_from_authorization_code(
.key
.sensitive_id;
Ok(ProvisionedApiKey {
Ok(CreatedApiKey {
organization_id: target.organization_id,
organization_title: target.organization_title,
default_project_id: target.project_id,
@@ -211,7 +211,7 @@ async fn exchange_authorization_code_for_tokens(
redirect_uri: &str,
code_verifier: &str,
code: &str,
) -> Result<OAuthTokens, ApiProvisionError> {
) -> Result<OAuthTokens, CreateApiKeyError> {
let url = format!("{}/oauth/token", issuer.trim_end_matches('/'));
execute_json(
client
@@ -241,7 +241,7 @@ async fn onboarding_login(
api_base: &str,
app: &str,
access_token: &str,
) -> Result<OnboardingLoginResponse, ApiProvisionError> {
) -> Result<OnboardingLoginResponse, CreateApiKeyError> {
let url = format!(
"{}/dashboard/onboarding/login",
api_base.trim_end_matches('/')
@@ -263,7 +263,7 @@ async fn list_organizations(
client: &Client,
api_base: &str,
session_key: &str,
) -> Result<Vec<Organization>, ApiProvisionError> {
) -> Result<Vec<Organization>, CreateApiKeyError> {
let url = format!("{}/v1/organizations", api_base.trim_end_matches('/'));
let response: DataList<Organization> = execute_json(
client
@@ -283,7 +283,7 @@ async fn list_projects(
api_base: &str,
session_key: &str,
organization_id: &str,
) -> Result<Vec<Project>, ApiProvisionError> {
) -> Result<Vec<Project>, CreateApiKeyError> {
let url = format!(
"{}/dashboard/organizations/{}/projects?detail=basic&limit=100",
api_base.trim_end_matches('/'),
@@ -309,7 +309,7 @@ async fn wait_for_default_project(
session_key: &str,
poll_interval_seconds: u64,
timeout_seconds: u64,
) -> Result<ProvisioningTarget, ApiProvisionError> {
) -> Result<ProvisioningTarget, CreateApiKeyError> {
let deadline = std::time::Instant::now() + Duration::from_secs(timeout_seconds);
loop {
let organizations = list_organizations(client, api_base, session_key).await?;
@@ -333,7 +333,7 @@ async fn wait_for_default_project(
};
if std::time::Instant::now() >= deadline {
return Err(ApiProvisionError::message(format!(
return Err(CreateApiKeyError::message(format!(
"Timed out waiting for an organization and default project. Last observed state: {last_state}"
)));
}
@@ -367,7 +367,7 @@ async fn create_project_api_key(
session_key: &str,
target: &ProvisioningTarget,
key_name: &str,
) -> Result<CreateApiKeyResponse, ApiProvisionError> {
) -> Result<CreateProjectApiKeyResponse, CreateApiKeyError> {
let url = format!(
"{}/dashboard/organizations/{}/projects/{}/api_keys",
api_base.trim_end_matches('/'),
@@ -394,26 +394,26 @@ async fn execute_json<T>(
request: reqwest::RequestBuilder,
method: &str,
url: &str,
) -> Result<T, ApiProvisionError>
) -> Result<T, CreateApiKeyError>
where
T: for<'de> Deserialize<'de>,
{
let response = request
.send()
.await
.map_err(|err| ApiProvisionError::message(format!("Network error calling {url}: {err}")))?;
.map_err(|err| CreateApiKeyError::message(format!("Network error calling {url}: {err}")))?;
let status = response.status();
let body = response.bytes().await.map_err(|err| {
ApiProvisionError::message(format!("Failed reading response from {url}: {err}"))
CreateApiKeyError::message(format!("Failed reading response from {url}: {err}"))
})?;
if !status.is_success() {
return Err(ApiProvisionError::api(
return Err(CreateApiKeyError::api(
format!("{method} {url} failed with HTTP {status}"),
String::from_utf8_lossy(&body).into_owned(),
));
}
serde_json::from_slice(&body)
.map_err(|err| ApiProvisionError::message(format!("{url} returned invalid JSON: {err}")))
.map_err(|err| CreateApiKeyError::message(format!("{url} returned invalid JSON: {err}")))
}
#[derive(Debug, Deserialize)]
@@ -472,21 +472,21 @@ struct ProvisioningTarget {
}
#[derive(Debug, Deserialize)]
struct CreateApiKeyResponse {
key: CreatedApiKey,
struct CreateProjectApiKeyResponse {
key: CreatedProjectApiKey,
}
#[derive(Debug, Deserialize)]
struct CreatedApiKey {
struct CreatedProjectApiKey {
sensitive_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiProvisionError {
pub struct CreateApiKeyError {
message: String,
}
impl ApiProvisionError {
impl CreateApiKeyError {
fn message(message: String) -> Self {
Self { message }
}
@@ -498,14 +498,14 @@ impl ApiProvisionError {
}
}
impl std::fmt::Display for ApiProvisionError {
impl std::fmt::Display for CreateApiKeyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ApiProvisionError {}
impl std::error::Error for CreateApiKeyError {}
#[cfg(test)]
#[path = "api_provision_tests.rs"]
#[path = "create_api_key_tests.rs"]
mod tests;
@@ -59,7 +59,7 @@ fn find_default_project_returns_initial_project() {
}
#[tokio::test]
async fn provision_from_authorization_code_provisions_api_key() {
async fn create_api_key_from_authorization_code_creates_api_key() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/oauth/token"))
@@ -129,7 +129,7 @@ async fn provision_from_authorization_code_provisions_api_key() {
.mount(&server)
.await;
let options = ApiProvisionOptions {
let options = CreateApiKeyOptions {
issuer: server.uri(),
client_id: "client-123".to_string(),
audience: PLATFORM_AUDIENCE.to_string(),
@@ -143,7 +143,7 @@ async fn provision_from_authorization_code_provisions_api_key() {
};
let client = build_http_client().expect("client");
let output = provision_from_authorization_code(
let output = create_api_key_from_authorization_code(
&client,
&options,
"http://localhost:5000/auth/callback",
@@ -155,7 +155,7 @@ async fn provision_from_authorization_code_provisions_api_key() {
assert_eq!(
output,
ProvisionedApiKey {
CreatedApiKey {
organization_id: "org-default".to_string(),
organization_title: Some("Default Org".to_string()),
default_project_id: "proj-default".to_string(),
+5 -5
View File
@@ -1,7 +1,7 @@
pub mod auth;
pub mod token_data;
mod api_provision;
mod create_api_key;
mod device_code_auth;
mod oauth_callback_server;
mod pkce;
@@ -17,10 +17,10 @@ pub use server::LoginServer;
pub use server::ServerOptions;
pub use server::run_login_server;
pub use api_provision::ApiProvisionError;
pub use api_provision::PendingApiProvisioning;
pub use api_provision::ProvisionedApiKey;
pub use api_provision::start_api_provisioning;
pub use create_api_key::CreateApiKeyError;
pub use create_api_key::CreatedApiKey;
pub use create_api_key::PendingCreateApiKey;
pub use create_api_key::start_create_api_key;
pub use auth::AuthConfig;
pub use auth::AuthCredentialsStoreMode;
pub use auth::AuthDotJson;
+3 -3
View File
@@ -291,7 +291,7 @@ use crate::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES;
use crate::status_indicator_widget::StatusDetailsCapitalization;
use crate::text_formatting::truncate_text;
use crate::tui::FrameRequester;
mod api_provision;
mod create_api_key;
mod dotenv_api_key;
mod interrupts;
use self::interrupts::InterruptManager;
@@ -4735,8 +4735,8 @@ impl ChatWidget {
tx.send(AppEvent::DiffResult(text));
});
}
SlashCommand::ApiProvision => {
self.start_api_provision();
SlashCommand::CreateApiKey => {
self.start_create_api_key();
}
SlashCommand::Copy => {
let Some(text) = self.last_copyable_output.as_deref() else {
@@ -6,10 +6,10 @@ use codex_core::AuthManager;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::auth::login_with_api_key;
use codex_core::auth::read_openai_api_key_from_env;
use codex_login::CreatedApiKey;
use codex_login::OPENAI_API_KEY_ENV_VAR;
use codex_login::PendingApiProvisioning;
use codex_login::ProvisionedApiKey;
use codex_login::start_api_provisioning;
use codex_login::PendingCreateApiKey;
use codex_login::start_create_api_key as start_create_api_key_flow;
use codex_protocol::config_types::ForcedLoginMethod;
use ratatui::style::Stylize;
use ratatui::text::Line;
@@ -23,8 +23,8 @@ use crate::history_cell;
use crate::history_cell::PlainHistoryCell;
impl ChatWidget {
pub(crate) fn start_api_provision(&mut self) {
match start_api_provision(
pub(crate) fn start_create_api_key(&mut self) {
match start_create_api_key_command(
self.app_event_tx.clone(),
self.auth_manager.clone(),
self.config.codex_home.clone(),
@@ -42,7 +42,7 @@ impl ChatWidget {
}
}
fn start_api_provision(
fn start_create_api_key_command(
app_event_tx: AppEventSender,
auth_manager: Arc<AuthManager>,
codex_home: PathBuf,
@@ -61,8 +61,8 @@ fn start_api_provision(
)
})?;
let session = start_api_provisioning()
.map_err(|err| format!("Failed to start API provisioning: {err}"))?;
let session = start_create_api_key_flow()
.map_err(|err| format!("Failed to start API key creation: {err}"))?;
let browser_opened = session.open_browser();
let start_message = continue_in_browser_message(
session.auth_url(),
@@ -91,10 +91,10 @@ fn start_api_provision(
fn existing_shell_api_key_message() -> PlainHistoryCell {
history_cell::new_info_event(
format!(
"{OPENAI_API_KEY_ENV_VAR} is already set in this Codex session; skipping API provisioning."
"{OPENAI_API_KEY_ENV_VAR} is already set in this Codex session; skipping API key creation."
),
Some(format!(
"This Codex session already inherited {OPENAI_API_KEY_ENV_VAR} from its shell environment. Unset it and run /api-provision again if you want Codex to provision and save a different key."
"This Codex session already inherited {OPENAI_API_KEY_ENV_VAR} from its shell environment. Unset it and run /create-api-key again if you want Codex to create and save a different key."
)),
)
}
@@ -108,7 +108,7 @@ fn continue_in_browser_message(
let mut lines = vec![
vec![
"• ".dim(),
"Finish API provisioning via your browser.".into(),
"Finish API key creation via your browser.".into(),
]
.into(),
"".into(),
@@ -122,7 +122,7 @@ fn continue_in_browser_message(
);
} else {
lines.push(
" Codex couldn't auto-open your browser, but the provisioning flow is still waiting."
" Codex couldn't auto-open your browser, but the API key creation flow is still waiting."
.dark_gray()
.into(),
);
@@ -156,7 +156,7 @@ fn continue_in_browser_message(
}
async fn complete_command(
session: PendingApiProvisioning,
session: PendingCreateApiKey,
dotenv_path: PathBuf,
codex_home: PathBuf,
forced_login_method: Option<ForcedLoginMethod>,
@@ -165,13 +165,13 @@ async fn complete_command(
let provisioned = match session.finish().await {
Ok(provisioned) => provisioned,
Err(err) => {
return history_cell::new_error_event(format!("API provisioning failed: {err}"));
return history_cell::new_error_event(format!("API key creation failed: {err}"));
}
};
if let Err(err) = upsert_dotenv_api_key(&dotenv_path, &provisioned.project_api_key) {
return history_cell::new_error_event(format!(
"Provisioning completed, but Codex could not update {}: {err}",
"API key creation completed, but Codex could not update {}: {err}",
dotenv_path.display()
));
}
@@ -210,7 +210,7 @@ fn live_apply_api_key(
}
fn success_cell(
provisioned: &ProvisionedApiKey,
provisioned: &CreatedApiKey,
dotenv_path: &Path,
live_apply_outcome: LiveApplyOutcome,
) -> PlainHistoryCell {
@@ -224,7 +224,7 @@ fn success_cell(
.unwrap_or_else(|| provisioned.default_project_id.clone());
let hint = match live_apply_outcome {
LiveApplyOutcome::Applied => Some(
"Updated this session to use the newly provisioned API key without touching auth.json."
"Updated this session to use the newly created API key without touching auth.json."
.to_string(),
),
LiveApplyOutcome::Skipped(reason) => Some(reason),
@@ -236,7 +236,7 @@ fn success_cell(
history_cell::new_info_event(
format!(
"Provisioned an API key for {organization} / {project} and saved {OPENAI_API_KEY_ENV_VAR} to {}.",
"Created an API key for {organization} / {project} and saved {OPENAI_API_KEY_ENV_VAR} to {}.",
dotenv_path.display()
),
hint,
@@ -258,7 +258,7 @@ mod tests {
#[test]
fn success_cell_snapshot() {
let cell = success_cell(
&ProvisionedApiKey {
&CreatedApiKey {
organization_id: "org-default".to_string(),
organization_title: Some("Default Org".to_string()),
default_project_id: "proj-default".to_string(),
@@ -275,7 +275,7 @@ mod tests {
#[test]
fn success_cell_snapshot_when_live_apply_is_skipped() {
let cell = success_cell(
&ProvisionedApiKey {
&CreatedApiKey {
organization_id: "org-default".to_string(),
organization_title: None,
default_project_id: "proj-default".to_string(),
@@ -310,7 +310,7 @@ mod tests {
assert_eq!(
render_cell(&cell),
"• OPENAI_API_KEY is already set in this Codex session; skipping API provisioning. This Codex session already inherited OPENAI_API_KEY from its shell environment. Unset it and run /api-provision again if you want Codex to provision and save a different key."
"• OPENAI_API_KEY is already set in this Codex session; skipping API key creation. This Codex session already inherited OPENAI_API_KEY from its shell environment. Unset it and run /create-api-key again if you want Codex to create and save a different key."
);
}
@@ -1,5 +0,0 @@
---
source: tui/src/chatwidget/api_provision.rs
expression: render_cell(&cell)
---
• Provisioned an API key for Default Org / Default Project and saved OPENAI_API_KEY to /tmp/workspace/.env.local. Updated this session to use the newly provisioned API key without touching auth.json.
@@ -1,5 +0,0 @@
---
source: tui/src/chatwidget/api_provision.rs
expression: render_cell(&cell)
---
• Provisioned an API key for org-default / proj-default and saved OPENAI_API_KEY to /tmp/workspace/.env.local. Saved OPENAI_API_KEY to .env.local, but left this session unchanged because ChatGPT login is required here.
@@ -1,10 +1,10 @@
---
source: tui/src/chatwidget/api_provision.rs
source: tui/src/chatwidget/create_api_key.rs
expression: render_cell(&cell)
---
• Finish API provisioning via your browser.
• Finish API key creation via your browser.
Codex couldn't auto-open your browser, but the provisioning flow is still waiting.
Codex couldn't auto-open your browser, but the API key creation flow is still waiting.
Open the following link to authenticate:
@@ -0,0 +1,5 @@
---
source: tui/src/chatwidget/create_api_key.rs
expression: render_cell(&cell)
---
• Created an API key for Default Org / Default Project and saved OPENAI_API_KEY to /tmp/workspace/.env.local. Updated this session to use the newly created API key without touching auth.json.
@@ -0,0 +1,5 @@
---
source: tui/src/chatwidget/create_api_key.rs
expression: render_cell(&cell)
---
• Created an API key for org-default / proj-default and saved OPENAI_API_KEY to /tmp/workspace/.env.local. Saved OPENAI_API_KEY to .env.local, but left this session unchanged because ChatGPT login is required here.
+9 -9
View File
@@ -34,7 +34,7 @@ pub enum SlashCommand {
Agent,
// Undo,
Diff,
ApiProvision,
CreateApiKey,
Copy,
Mention,
Status,
@@ -83,7 +83,7 @@ impl SlashCommand {
// SlashCommand::Undo => "ask Codex to undo a turn",
SlashCommand::Quit | SlashCommand::Exit => "exit Codex",
SlashCommand::Diff => "show git diff (including untracked files)",
SlashCommand::ApiProvision => "provision an API key and save it to .env.local",
SlashCommand::CreateApiKey => "create an API key and save it to .env.local",
SlashCommand::Copy => "copy the latest Codex output to your clipboard",
SlashCommand::Mention => "mention a file",
SlashCommand::Skills => "use skills to improve how Codex performs specific tasks",
@@ -157,7 +157,7 @@ impl SlashCommand {
| SlashCommand::Experimental
| SlashCommand::Review
| SlashCommand::Plan
| SlashCommand::ApiProvision
| SlashCommand::CreateApiKey
| SlashCommand::Clear
| SlashCommand::Logout
| SlashCommand::MemoryDrop
@@ -225,13 +225,13 @@ mod tests {
}
#[test]
fn api_provision_command_metadata() {
fn create_api_key_command_metadata() {
assert_eq!(
SlashCommand::from_str("api-provision"),
Ok(SlashCommand::ApiProvision)
SlashCommand::from_str("create-api-key"),
Ok(SlashCommand::CreateApiKey)
);
assert_eq!(SlashCommand::ApiProvision.command(), "api-provision");
assert!(!SlashCommand::ApiProvision.supports_inline_args());
assert!(!SlashCommand::ApiProvision.available_during_task());
assert_eq!(SlashCommand::CreateApiKey.command(), "create-api-key");
assert!(!SlashCommand::CreateApiKey.supports_inline_args());
assert!(!SlashCommand::CreateApiKey.available_during_task());
}
}