Skip background workspace routing reads without ChatGPT auth (#52635)

## Why

Background workspace routing reads reload configuration even when signed out or using an API key, although routing requires ChatGPT authentication.

## What changed

Check cached authentication before reading accounts for workspace routing at app-server startup and connection initialization. Skip these background reads without ChatGPT authentication, while preserving configuration reloads for explicit `account/read` requests.

## Testing

Add regression tests covering signed-out and API-key startup and connection initialization, explicit `account/read` configuration reloads, and routing after ChatGPT sign-in, including propagation of configuration errors.

GitOrigin-RevId: 8b90ee609ed24f8618cd21f160eb7001703b8bd1
This commit is contained in:
Joe Savona
2026-10-09 16:40:52 +00:00
committed by copyberry
parent 7f2f4fd46e
commit 751e7edd0b
5 changed files with 304 additions and 3 deletions
@@ -150,7 +150,9 @@ impl AccountRequestProcessor {
.set_workspace_routing_resolver(Arc::downgrade(&resolver));
let startup = processor.clone();
tokio::spawn(async move {
let _ = startup.read_account(/*request*/ None).await;
let _ = startup
.read_account_for_background_workspace_routing()
.await;
});
processor
}
@@ -128,6 +128,21 @@ impl WorkspaceRoutingResolver for AccountRequestProcessor {
}
impl AccountRequestProcessor {
pub(super) async fn read_account_for_background_workspace_routing(
&self,
) -> Result<Option<AccountRead>, AccountReadError> {
if !self
.auth_manager
.auth_cached()
.as_ref()
.is_some_and(CodexAuth::is_chatgpt_auth)
{
return Ok(None);
}
self.read_account(/*request*/ None).await.map(Some)
}
pub(crate) fn notify_workspace_routing_to_connection(&self, connection_id: ConnectionId) {
let processor = self.clone();
let auth_changes = self.auth_manager.auth_change_state_receiver();
@@ -136,7 +151,9 @@ impl AccountRequestProcessor {
if auth_changes.borrow().owner_generation != owner_generation {
return;
}
if let Ok(response) = processor.read_account(/*request*/ None).await
if let Ok(Some(response)) = processor
.read_account_for_background_workspace_routing()
.await
&& response.workspace_routing.is_some()
&& auth_changes.borrow().owner_generation == owner_generation
{
@@ -1,10 +1,136 @@
//! Origin resolution and strict discovery validation.
//! Origin resolution, strict discovery validation, and background read eligibility.
use super::*;
use pretty_assertions::assert_eq;
use serde_json::Value;
use test_case::test_case;
#[test_case(None; "signed_out")]
#[test_case(Some("dummy"); "api_key")]
#[tokio::test]
async fn background_routing_skips_config_until_chatgpt_sign_in(
api_key: Option<&str>,
) -> anyhow::Result<()> {
use app_test_support::ChatGptAuthFixture;
use app_test_support::write_chatgpt_auth;
use codex_config::CloudConfigBundleLoader;
use codex_config::LoaderOverrides;
use codex_config::types::AuthCredentialsStoreMode;
use codex_exec_server::EnvironmentManager;
use codex_login::AuthKeyringBackendKind;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
let home = tempfile::tempdir()?;
let backend = wiremock::MockServer::start().await;
app_test_support::mount_workspace_routing(&backend).await;
let loads = Arc::new(AtomicUsize::new(/*v*/ 0));
let calls = Arc::clone(&loads);
let loader = CloudConfigBundleLoader::from_getter(move || {
calls.fetch_add(/*val*/ 1, Ordering::SeqCst);
async { Ok(None) }
});
let manager = ConfigManager::new_for_tests(
home.path().to_path_buf(),
vec![(
"chatgpt_base_url".into(),
format!("{}/backend-api", backend.uri()).into(),
)],
LoaderOverrides::without_managed_config_for_tests(),
loader,
);
let config = Arc::new(
manager
.load_latest_config(Some(home.path().to_path_buf()))
.await?,
);
if let Some(api_key) = api_key {
codex_login::login_with_api_key(
home.path(),
api_key,
AuthCredentialsStoreMode::File,
AuthKeyringBackendKind::default(),
)?;
}
let auth_manager = Arc::new(
AuthManager::new(
home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*forced_chatgpt_workspace_id*/ None,
Some(config.chatgpt_base_url.clone()),
AuthKeyringBackendKind::default(),
config.auth_config().auth_route_config,
)
.await,
);
let threads = Arc::new(
codex_core::test_support::thread_manager_with_models_provider_and_home(
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
Arc::new(EnvironmentManager::default_for_tests()),
),
);
let (sender, _receiver) = tokio::sync::mpsc::channel(16);
let outgoing = Arc::new(OutgoingMessageSender::new(
sender,
codex_analytics::AnalyticsEventsClient::disabled(),
));
let processor = AccountRequestProcessor::new(
Arc::clone(&auth_manager),
threads,
outgoing,
config,
manager,
);
let initial_loads = loads.load(Ordering::SeqCst);
for _ in 0..3 {
assert!(
processor
.read_account_for_background_workspace_routing()
.await?
.is_none()
);
}
assert_eq!(loads.load(Ordering::SeqCst), initial_loads);
processor.read_account(/*request*/ None).await?;
assert!(loads.load(Ordering::SeqCst) > initial_loads);
write_chatgpt_auth(
home.path(),
ChatGptAuthFixture::new("access-chatgpt").account_id("account-123"),
AuthCredentialsStoreMode::File,
)?;
assert!(auth_manager.reload().await);
let initial_loads = loads.load(Ordering::SeqCst);
let read = processor
.read_account_for_background_workspace_routing()
.await?
.unwrap();
assert_eq!(
read.workspace_routing,
Some(WorkspaceRouting {
chatgpt_account_id: "account-123".into(),
backend_origin: "https://chatgpt.com".into(),
account_routing_override: "NO_CONSTRAINT".into(),
})
);
assert!(loads.load(Ordering::SeqCst) > initial_loads);
std::fs::write(home.path().join("config.toml"), "invalid = [")?;
assert!(matches!(
processor
.read_account_for_background_workspace_routing()
.await,
Err(AccountReadError::Routing(
WorkspaceRoutingError::RequirementsLoad
))
));
Ok(())
}
#[test_case(Some("https://gov.chatgpt.com/backend-api/"), "NO_CONSTRAINT", "https://gov.chatgpt.com"; "configured_only")]
#[test_case(None, "https://gov.chatgpt.com", "https://gov.chatgpt.com"; "discovered_only")]
#[test_case(Some("https://GOV.chatgpt.com:443/backend-api/"), "https://gov.chatgpt.com", "https://gov.chatgpt.com"; "matching_effective_port")]