codex: set created API keys in session env

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 a19cf09ac5
commit 39821795d4
4 changed files with 76 additions and 72 deletions
+5
View File
@@ -8,6 +8,7 @@ use crate::file_watcher::WatchRegistration;
use crate::protocol::Event;
use crate::protocol::Op;
use crate::protocol::Submission;
use std::collections::HashMap;
use codex_features::Feature;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::Personality;
@@ -122,6 +123,10 @@ impl CodexThread {
self.codex.agent_status().await
}
pub async fn set_dependency_env(&self, values: HashMap<String, String>) {
self.codex.set_dependency_env(values).await;
}
pub(crate) fn subscribe_status(&self) -> watch::Receiver<AgentStatus> {
self.codex.agent_status.clone()
}
+15
View File
@@ -3117,6 +3117,21 @@ impl App {
AppEvent::RefreshConnectors { force_refetch } => {
self.chat_widget.refresh_connectors(force_refetch);
}
AppEvent::SetDependencyEnv { values, result_tx } => {
let result = match self.active_thread_id {
Some(thread_id) => match self.server.get_thread(thread_id).await {
Ok(thread) => {
thread.set_dependency_env(values).await;
Ok(())
}
Err(err) => Err(format!(
"failed to load active Codex thread for dependency env update: {err}"
)),
},
None => Err("no active Codex thread for dependency env update".to_string()),
};
let _ = result_tx.send(result);
}
AppEvent::PluginInstallAuthAdvance { refresh_connectors } => {
if refresh_connectors {
self.chat_widget.refresh_connectors(/*force_refetch*/ true);
+8
View File
@@ -8,6 +8,7 @@
//! Exit is modelled explicitly via `AppEvent::Exit(ExitMode)` so callers can request shutdown-first
//! quits without reaching into the app loop or coupling to shutdown/exit sequencing.
use std::collections::HashMap;
use std::path::PathBuf;
use codex_app_server_protocol::PluginInstallResponse;
@@ -37,6 +38,7 @@ use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use tokio::sync::oneshot;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RealtimeAudioDeviceKind {
@@ -168,6 +170,12 @@ pub(crate) enum AppEvent {
force_refetch: bool,
},
/// Add environment variables to the active thread's dependency env override.
SetDependencyEnv {
values: HashMap<String, String>,
result_tx: oneshot::Sender<Result<(), String>>,
},
/// Fetch plugin marketplace state for the provided working directory.
FetchPluginsList {
cwd: PathBuf,
+48 -72
View File
@@ -1,18 +1,13 @@
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::collections::HashMap;
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::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;
use tokio::sync::oneshot;
use super::ChatWidget;
use crate::app_event::AppEvent;
@@ -23,12 +18,7 @@ use crate::history_cell::PlainHistoryCell;
impl ChatWidget {
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(),
self.config.forced_login_method,
) {
match start_create_api_key_command(self.app_event_tx.clone()) {
Ok(start_message) => {
self.add_to_history(start_message);
self.request_redraw();
@@ -42,9 +32,6 @@ impl ChatWidget {
fn start_create_api_key_command(
app_event_tx: AppEventSender,
auth_manager: Arc<AuthManager>,
codex_home: PathBuf,
forced_login_method: Option<ForcedLoginMethod>,
) -> Result<PlainHistoryCell, String> {
if read_openai_api_key_from_env().is_some() {
return Ok(existing_shell_api_key_message());
@@ -61,7 +48,7 @@ fn start_create_api_key_command(
let app_event_tx_for_task = app_event_tx;
tokio::spawn(async move {
let cell = complete_command(session, codex_home, forced_login_method, auth_manager).await;
let cell = complete_command(session, app_event_tx_for_task.clone()).await;
app_event_tx_for_task.send(AppEvent::InsertHistoryCell(Box::new(cell)));
});
@@ -133,9 +120,7 @@ fn continue_in_browser_message(
async fn complete_command(
session: PendingCreateApiKey,
codex_home: PathBuf,
forced_login_method: Option<ForcedLoginMethod>,
auth_manager: Arc<AuthManager>,
app_event_tx: AppEventSender,
) -> PlainHistoryCell {
let provisioned = match session.finish().await {
Ok(provisioned) => provisioned,
@@ -144,44 +129,49 @@ async fn complete_command(
}
};
let copy_result = clipboard_text::copy_text_to_clipboard(&provisioned.project_api_key);
success_cell(
&provisioned,
copy_result,
live_apply_api_key(
forced_login_method,
&codex_home,
&provisioned.project_api_key,
auth_manager,
),
let session_env_result = apply_api_key_to_current_session(
&provisioned.project_api_key,
app_event_tx,
)
.await;
success_cell(&provisioned, copy_result, session_env_result)
}
fn live_apply_api_key(
forced_login_method: Option<ForcedLoginMethod>,
codex_home: &Path,
async fn apply_api_key_to_current_session(
api_key: &str,
auth_manager: Arc<AuthManager>,
) -> LiveApplyOutcome {
if matches!(forced_login_method, Some(ForcedLoginMethod::Chatgpt)) {
return LiveApplyOutcome::Skipped(format!(
"Created {OPENAI_API_KEY_ENV_VAR}, but left this session unchanged because ChatGPT login is required here."
));
}
app_event_tx: AppEventSender,
) -> Result<(), String> {
set_current_process_api_key(api_key);
match login_with_api_key(codex_home, api_key, AuthCredentialsStoreMode::Ephemeral) {
Ok(()) => {
auth_manager.reload();
LiveApplyOutcome::Applied
}
Err(err) => LiveApplyOutcome::Failed(err.to_string()),
let (result_tx, result_rx) = oneshot::channel();
app_event_tx.send(AppEvent::SetDependencyEnv {
values: HashMap::from([(OPENAI_API_KEY_ENV_VAR.to_string(), api_key.to_string())]),
result_tx,
});
match result_rx.await {
Ok(result) => result,
Err(err) => Err(format!(
"dependency env update response channel closed before completion: {err}"
)),
}
}
fn set_current_process_api_key(api_key: &str) {
// SAFETY: `/create-api-key` intentionally mutates process-global environment so the running
// Codex session can observe `OPENAI_API_KEY` immediately. This is scoped to a single
// user-triggered command, and spawned tool environments are updated separately through the
// session dependency env override.
unsafe {
std::env::set_var(OPENAI_API_KEY_ENV_VAR, api_key);
}
}
fn success_cell(
provisioned: &CreatedApiKey,
copy_result: Result<(), String>,
live_apply_outcome: LiveApplyOutcome,
session_env_result: Result<(), String>,
) -> PlainHistoryCell {
let organization = provisioned
.organization_title
@@ -196,20 +186,15 @@ fn success_cell(
Ok(()) => "Copied the full key to your clipboard.".to_string(),
Err(err) => format!("Could not copy the key to your clipboard: {err}"),
};
let live_apply_status = match live_apply_outcome {
LiveApplyOutcome::Applied => Some(
"Updated this session to use the newly created API key without touching auth.json."
.to_string(),
),
LiveApplyOutcome::Skipped(reason) => Some(reason),
LiveApplyOutcome::Failed(err) => Some(format!(
"Created {OPENAI_API_KEY_ENV_VAR}, but could not hot-apply it in this session: {err}",
)),
let session_env_status = match session_env_result {
Ok(()) => {
format!("Set {OPENAI_API_KEY_ENV_VAR} in this Codex session for spawned commands.")
}
Err(err) => {
format!("Could not set {OPENAI_API_KEY_ENV_VAR} in this Codex session: {err}")
}
};
let hint = Some(match live_apply_status {
Some(live_apply_status) => format!("{copy_status} {live_apply_status}"),
None => copy_status,
});
let hint = Some(format!("{copy_status} {session_env_status}"));
history_cell::new_info_event(
format!(
@@ -234,12 +219,6 @@ fn mask_api_key(api_key: &str) -> String {
)
}
enum LiveApplyOutcome {
Applied,
Skipped(String),
Failed(String),
}
#[cfg(test)]
mod tests {
use super::*;
@@ -257,14 +236,14 @@ mod tests {
project_api_key: "sk-proj-123".to_string(),
},
Ok(()),
LiveApplyOutcome::Applied,
Ok(()),
);
assert_snapshot!(render_cell(&cell));
}
#[test]
fn success_cell_snapshot_when_live_apply_is_skipped() {
fn success_cell_snapshot_when_clipboard_copy_fails() {
let cell = success_cell(
&CreatedApiKey {
organization_id: "org-default".to_string(),
@@ -274,10 +253,7 @@ mod tests {
project_api_key: "sk-proj-123".to_string(),
},
Err("clipboard unavailable".to_string()),
LiveApplyOutcome::Skipped(
"Created OPENAI_API_KEY, but left this session unchanged because ChatGPT login is required here."
.to_string(),
),
Err("dependency env unavailable".to_string()),
);
assert_snapshot!(render_cell(&cell));