[codex-cli] eagerly refresh ChatGPT access token on startup [ci changed_files]

This commit is contained in:
Cooper Gamble
2026-05-19 20:13:55 +00:00
parent 16d85e2708
commit d17c381d3f
4 changed files with 91 additions and 5 deletions

View File

@@ -51,7 +51,7 @@ use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnStartedNotification;
use codex_arg0::Arg0DispatchPaths;
use codex_cloud_requirements::cloud_requirements_loader_for_storage;
use codex_cloud_requirements::cloud_requirements_loader;
use codex_config::ConfigLoadError;
use codex_config::ConfigLoadOptions;
use codex_config::LoaderOverrides;
@@ -71,6 +71,7 @@ use codex_core::path_utils;
use codex_feedback::CodexFeedback;
use codex_git_utils::get_git_repo_root;
use codex_login::AuthConfig;
use codex_login::AuthManager;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_login::default_client::set_default_originator;
use codex_login::enforce_login_restrictions;
@@ -363,13 +364,21 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
.clone()
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
// TODO(gt): Make cloud requirements failures blocking once we can fail-closed.
let cloud_requirements = cloud_requirements_loader_for_storage(
let cloud_auth_manager = AuthManager::shared(
codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
config_toml.cli_auth_credentials_store.unwrap_or_default(),
chatgpt_base_url,
Some(chatgpt_base_url.clone()),
)
.await;
if let Err(err) = cloud_auth_manager.refresh_managed_chatgpt_token().await {
warn!("failed to proactively refresh ChatGPT access token during CLI startup: {err}");
}
let cloud_requirements = cloud_requirements_loader(
cloud_auth_manager,
chatgpt_base_url,
codex_home.to_path_buf(),
);
let run_cli_overrides = cli_kv_overrides.clone();
let run_loader_overrides = loader_overrides.clone();
let run_cloud_requirements = cloud_requirements.clone();

View File

@@ -1715,6 +1715,19 @@ impl AuthManager {
}
}
/// Refresh managed ChatGPT auth even when its access token has not expired yet.
///
/// CLI startup uses this to begin sessions with a freshly issued access token.
/// Other auth modes either cannot be refreshed locally or have separate refresh
/// ownership, so they intentionally no-op here.
pub async fn refresh_managed_chatgpt_token(&self) -> Result<(), RefreshTokenError> {
if !matches!(self.auth_cached(), Some(CodexAuth::Chatgpt(_))) {
return Ok(());
}
self.refresh_token().await
}
/// Attempt to refresh the current auth token from the authority that issued
/// the token. On success, reloads the auth state from disk so other components
/// observe refreshed token. If the token refresh fails, returns the error to

View File

@@ -158,6 +158,60 @@ async fn refresh_token_refreshes_when_auth_is_unchanged() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[tokio::test]
async fn refresh_managed_chatgpt_token_refreshes_even_when_auth_is_fresh() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/oauth/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"access_token": "new-access-token",
"refresh_token": "new-refresh-token"
})))
.expect(1)
.mount(&server)
.await;
let ctx = RefreshTokenTestContext::new(&server).await?;
let initial_last_refresh = Utc::now();
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
let initial_auth = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(initial_tokens.clone()),
last_refresh: Some(initial_last_refresh),
agent_identity: None,
};
ctx.write_auth(&initial_auth).await?;
ctx.auth_manager
.refresh_managed_chatgpt_token()
.await
.context("managed ChatGPT refresh should succeed")?;
let refreshed_tokens = TokenData {
access_token: "new-access-token".to_string(),
refresh_token: "new-refresh-token".to_string(),
..initial_tokens.clone()
};
let stored = ctx.load_auth()?;
let tokens = stored.tokens.as_ref().context("tokens should exist")?;
assert_eq!(tokens, &refreshed_tokens);
let refreshed_at = stored
.last_refresh
.as_ref()
.context("last_refresh should be recorded")?;
assert!(
*refreshed_at >= initial_last_refresh,
"last_refresh should advance"
);
server.verify().await;
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[tokio::test]
async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> {

View File

@@ -37,6 +37,7 @@ use codex_app_server_protocol::ThreadListCwdFilter;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadSortKey as AppServerThreadSortKey;
use codex_app_server_protocol::ThreadSourceKind;
use codex_cloud_requirements::cloud_requirements_loader;
use codex_cloud_requirements::cloud_requirements_loader_for_storage;
use codex_config::CloudRequirementsLoader;
use codex_config::ConfigLoadError;
@@ -45,6 +46,7 @@ use codex_config::format_config_error_with_source;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecServerRuntimePaths;
use codex_login::AuthConfig;
use codex_login::AuthManager;
use codex_login::default_client::originator;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_login::enforce_login_restrictions;
@@ -985,13 +987,21 @@ pub async fn run_main(
.chatgpt_base_url
.clone()
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
let cloud_requirements = cloud_requirements_loader_for_storage(
let cloud_auth_manager = AuthManager::shared(
codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
config_toml.cli_auth_credentials_store.unwrap_or_default(),
chatgpt_base_url,
Some(chatgpt_base_url.clone()),
)
.await;
if let Err(err) = cloud_auth_manager.refresh_managed_chatgpt_token().await {
warn!("failed to proactively refresh ChatGPT access token during CLI startup: {err}");
}
let cloud_requirements = cloud_requirements_loader(
cloud_auth_manager,
chatgpt_base_url,
codex_home.to_path_buf(),
);
let model_provider_override = if cli.oss {
let resolved = resolve_oss_provider(