Integrate config-server policies with startup and authentication (#57175)

## What changed

- Use the managed configuration loader in CLI, exec, TUI, and app-server configuration flows so `requirements.toml` can select a config server.
- Sign in to the config server when needed during interactive TUI startup and CLI login, reload configuration after CLI sign-in, and clear config-server credentials on CLI logout.
- Preserve the app-server's config-server settings and refresh worker across OpenAI login and logout.
- Require config-server settings to be applied when loading CLI and doctor configuration.

## Testing

Add app-server integration tests using a local HTTPS config server to verify managed provider and instruction settings at startup, preservation across API-key login and logout, and cache reuse after restart and thread resume.

GitOrigin-RevId: 27ee81f7943d2290a5c3fcc0ac556579ec63f8b4
This commit is contained in:
smurching-oai
2026-10-26 18:14:57 +00:00
committed by copyberry
parent 619531225b
commit ec513795a6
13 changed files with 401 additions and 39 deletions
+21 -18
View File
@@ -1,5 +1,5 @@
use codex_arg0::Arg0DispatchPaths;
use codex_cloud_config::chatgpt_config_bundle_loader;
use codex_cloud_config::managed_config_bundle_loader;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLayerStack;
use codex_config::LoaderOverrides;
@@ -196,23 +196,24 @@ impl ConfigManager {
auth_manager: Arc<AuthManager>,
config: &Config,
) {
let chatgpt_base_url = config.chatgpt_base_url.clone();
let http_client_factory = config.http_client_factory();
let endpoint = codex_backend_client::Client::new(
chatgpt_base_url.clone(),
http_client_factory.clone(),
)
.config_bundle_url();
let http_client_factory = http_client_factory.with_network_policy(
self.local_network_policy
.policy()
.restrict_to_endpoints(endpoint.parse().into_iter().collect()),
);
let loader = chatgpt_config_bundle_loader(
// Keep config-server settings and their refresh worker when only
// ChatGPT/API-key credentials change.
if self
.current_managed_config_loader()
.config_server()
.is_some()
{
self.revoke_mcp_ema();
return;
}
let loader = managed_config_bundle_loader(
auth_manager,
chatgpt_base_url,
config.chatgpt_base_url.clone(),
self.codex_home.clone(),
http_client_factory,
config
.http_client_factory()
.with_network_policy(self.local_network_policy.policy()),
config.config_layer_stack.requirements_toml(),
);
if let Ok(mut guard) = self.cloud_config_bundle.write() {
guard.retire_ema_policy();
@@ -225,8 +226,10 @@ impl ConfigManager {
pub(crate) fn clear_chatgpt_config_loader(&self) {
if let Ok(mut guard) = self.cloud_config_bundle.write() {
guard.retire_ema_policy();
*guard = CloudConfigBundleLoader::default();
if guard.config_server().is_none() {
guard.retire_ema_policy();
*guard = CloudConfigBundleLoader::default();
}
} else {
warn!("failed to clear cloud config bundle loader");
}
@@ -0,0 +1,241 @@
//! Exercises required HTTPS policy through real app-server startup and session RPCs.
use anyhow::Result;
use app_test_support::TestAppServer;
use codex_app_server_protocol::LoginAccountResponse;
use codex_app_server_protocol::LogoutAccountResponse;
use codex_app_server_protocol::ThreadResumeParams;
use codex_app_server_protocol::ThreadResumeResponse;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UserInput;
use codex_config::test_support::CloudConfigBundleFixture;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
use tempfile::tempdir;
use tokio::net::TcpListener;
use tokio::net::TcpStream;
use tokio_rustls::TlsAcceptor;
use tokio_rustls::rustls;
use tokio_util::task::AbortOnDropHandle;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_server_settings_are_fetched_and_applied_at_startup() -> Result<()> {
skip_if_no_network!(Ok(()));
let setup = ConfigServerSetup::new().await?;
let mut app = setup.start_app().await?;
assert_eq!(
setup.config_server.received_requests().await.unwrap().len(),
1
);
let started = app
.start_thread(ThreadStartParams {
model_provider: Some("openai".into()),
..Default::default()
})
.await?;
assert_eq!(started.model_provider, "company");
setup
.assert_turn_uses_managed_instructions(&mut app, &started.thread.id)
.await?;
app.shutdown_gracefully().await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_server_settings_survive_openai_login_and_logout() -> Result<()> {
skip_if_no_network!(Ok(()));
let setup = ConfigServerSetup::new().await?;
let mut app = setup.start_app().await?;
let login = app
.send_login_account_api_key_request("test-api-key")
.await?;
let _: LoginAccountResponse = app.read_response(login).await?;
let logout = app.send_logout_account_request().await?;
let _: LogoutAccountResponse = app.read_response(logout).await?;
let started = app.start_thread(ThreadStartParams::default()).await?;
setup
.assert_turn_uses_managed_instructions(&mut app, &started.thread.id)
.await?;
app.shutdown_gracefully().await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_server_settings_are_cached_across_restart_and_resume() -> Result<()> {
skip_if_no_network!(Ok(()));
let setup = ConfigServerSetup::new().await?;
let mut app = setup.start_app().await?;
let started = app.start_thread(ThreadStartParams::default()).await?;
setup
.assert_turn_uses_managed_instructions(&mut app, &started.thread.id)
.await?;
app.shutdown_gracefully().await?;
let mut app = setup.start_app().await?;
let resumed = app
.send_thread_resume_request(ThreadResumeParams {
thread_id: started.thread.id,
..Default::default()
})
.await?;
let resumed: ThreadResumeResponse = app.read_response(resumed).await?;
setup
.assert_turn_uses_managed_instructions(&mut app, &resumed.thread.id)
.await?;
assert_eq!(
setup.config_server.received_requests().await.unwrap().len(),
1
);
app.shutdown_gracefully().await?;
Ok(())
}
struct ConfigServerSetup {
home: TempDir,
config_server: MockServer,
inference: MockServer,
ca_path: PathBuf,
_tls: AbortOnDropHandle<()>,
}
impl ConfigServerSetup {
async fn new() -> Result<Self> {
let home = tempdir()?;
let config_server = MockServer::start().await;
let inference = responses::start_mock_server().await;
let requirements = format!(
r#"additional_developer_instructions = 'Company requirements'
model_provider = 'company'
[model_providers.company]
name = 'Company'
base_url = {:?}
env_key = 'CONFIG_SERVER_TEST_KEY'
wire_api = 'responses'
"#,
inference.uri()
);
let bundle = CloudConfigBundleFixture::enterprise_config(
"developer_instructions = 'Company defaults'",
)
.add_enterprise_requirement(requirements)
.into_bundle();
Mock::given(method("GET"))
.and(path("/team/codex/config/bundle"))
.respond_with(ResponseTemplate::new(200).set_body_json(bundle))
.mount(&config_server)
.await;
let (origin, ca_path, tls) = https_mock_server(home.path(), &config_server).await?;
let base_url = format!("{origin}/team/");
std::fs::write(
home.path().join("requirements.toml"),
format!("[config_server]\nbase_url = {base_url:?}\n"),
)?;
std::fs::write(
home.path().join("config.toml"),
"model = 'gpt-5.5'\napproval_policy = 'never'\nsandbox_mode = 'read-only'\n[features]\napps = false\n",
)?;
Ok(Self {
home,
config_server,
inference,
ca_path,
_tls: tls,
})
}
async fn start_app(&self) -> Result<TestAppServer> {
let ca_path = self.ca_path.to_string_lossy();
let env = [
("CODEX_CA_CERTIFICATE", Some(ca_path.as_ref())),
("CONFIG_SERVER_TEST_KEY", Some("test-key")),
("NO_PROXY", Some("127.0.0.1,localhost")),
];
TestAppServer::builder()
.with_codex_home(self.home.path())
.with_args(&["--strict-config"])
.with_env_overrides(&env)
.build_initialized()
.await
}
async fn assert_turn_uses_managed_instructions(
&self,
app: &mut TestAppServer,
thread_id: &str,
) -> Result<()> {
let recorded = responses::mount_sse_once(
&self.inference,
responses::sse(vec![
responses::ev_assistant_message("reply", "Policy applied"),
responses::ev_completed("response"),
]),
)
.await;
let completed = app
.start_turn_and_wait_for_completion(TurnStartParams {
thread_id: thread_id.into(),
input: vec![UserInput::Text {
text: "Hello".into(),
text_elements: vec![],
}],
..Default::default()
})
.await?;
assert_eq!(completed.turn.status, TurnStatus::Completed);
let instructions = recorded
.single_request()
.message_input_texts("developer")
.join("\n");
assert!(instructions.contains("Company defaults"));
assert!(instructions.contains("Company requirements"));
Ok(())
}
}
async fn https_mock_server(
home: &Path,
policy: &MockServer,
) -> Result<(String, PathBuf, AbortOnDropHandle<()>)> {
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into(), "127.0.0.1".into()])?;
let ca_path = home.join("policy-ca.pem");
std::fs::write(&ca_path, cert.cert.pem())?;
let tls = rustls::ServerConfig::builder_with_provider(Arc::new(
rustls::crypto::aws_lc_rs::default_provider(),
))
.with_safe_default_protocol_versions()?
.with_no_client_auth()
.with_single_cert(
vec![cert.cert.der().clone()],
rustls::pki_types::PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()).into(),
)?;
let acceptor = TlsAcceptor::from(Arc::new(tls));
let listener = TcpListener::bind("127.0.0.1:0").await?;
let origin_url = format!("https://{}", listener.local_addr()?);
let origin = *policy.address();
let tls = AbortOnDropHandle::new(tokio::spawn(async move {
while let Ok((stream, _)) = listener.accept().await {
let acceptor = acceptor.clone();
tokio::spawn(async move {
let mut stream = acceptor.accept(stream).await?;
let mut upstream = TcpStream::connect(origin).await?;
tokio::io::copy_bidirectional(&mut stream, &mut upstream).await?;
Ok::<_, std::io::Error>(())
});
}
}));
Ok((origin_url, ca_path, tls))
}
@@ -38,6 +38,8 @@ mod config_requirements_exec;
mod config_requirements_in_app_browser;
mod config_requirements_login;
mod config_rpc;
#[path = "config_server_tests.rs"]
mod config_server;
#[cfg(unix)]
#[path = "connection_handling_stdio_tests.rs"]
mod connection_handling_stdio;
+4 -2
View File
@@ -1,7 +1,7 @@
use anyhow::Context;
use anyhow::Result;
use codex_app_server::in_process::EmbeddedNetworkPolicy;
use codex_cloud_config::chatgpt_config_bundle_loader_for_storage;
use codex_cloud_config::managed_config_bundle_loader_for_storage;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLoadOptions;
use codex_core::config::Config;
@@ -27,6 +27,7 @@ pub async fn load_config(
.build()
.await
.context("failed to load configuration")?;
config.config_layer_stack.ensure_config_server_applied()?;
EmbeddedNetworkPolicy::default().activate(&mut config);
Ok(config)
}
@@ -58,11 +59,12 @@ pub async fn config_builder(
)
.await
.context("failed to load bootstrap configuration")?;
let cloud_config_bundle = chatgpt_config_bundle_loader_for_storage(
let cloud_config_bundle = managed_config_bundle_loader_for_storage(
network_policy.bind_bootstrap_auth(
bootstrap_auth_config(codex_home.as_path(), &bootstrap_config)
.context("failed to resolve cloud configuration authentication")?,
),
bootstrap_config.config_layer_stack.requirements_toml(),
/*enable_codex_api_key_env*/ false,
)
.await
+4 -2
View File
@@ -628,7 +628,7 @@ async fn load_config(
..config_overrides_from_interactive(interactive, arg0_paths)
};
crate::cloud_config::config_builder(
let config = crate::cloud_config::config_builder(
&root_config_overrides,
LoaderOverrides::default(),
overrides,
@@ -636,7 +636,9 @@ async fn load_config(
.await?
.build()
.await
.context("failed to load Codex config")
.context("failed to load Codex config")?;
config.config_layer_stack.ensure_config_server_applied()?;
Ok(config)
}
fn config_overrides_from_interactive(
+1 -1
View File
@@ -18,7 +18,7 @@ pub use login::read_api_key_from_stdin;
pub use login::run_login_status;
pub use login::run_login_with_access_token;
pub use login::run_login_with_api_key;
pub use login::run_login_with_chatgpt;
pub use login::run_login_with_browser;
pub use login::run_login_with_device_code;
pub use login::run_login_with_device_code_fallback_to_browser;
pub use login::run_logout;
+79 -7
View File
@@ -172,6 +172,27 @@ pub async fn login_with_chatgpt(
server.block_until_done().await
}
/// Signs into the config server if needed, then reloads configuration.
async fn sign_in_to_config_server_and_reload_config(
config: &mut Config,
cli_config_overrides: &CliConfigOverrides,
) -> std::io::Result<()> {
let requirements = config.config_layer_stack.requirements_toml();
let network = codex_app_server::in_process::EmbeddedNetworkPolicy::load(
&codex_config::LoaderOverrides::default(),
)
.await;
let bootstrap_auth = network.bind_bootstrap_auth(config.auth_config());
let auth_route_config = bootstrap_auth.auth_route_config.clone();
codex_cloud_config::ensure_config_server_login(bootstrap_auth, requirements).await?;
*config = crate::cloud_config::load_config(cli_config_overrides, Default::default())
.await
.map_err(|error| std::io::Error::other(format!("{error:#}")))?;
network.activate(config);
config.application_auth_route_config = Some(auth_route_config);
Ok(())
}
fn finish_login(result: std::io::Result<()>) -> ! {
match result {
Ok(_) => {
@@ -187,17 +208,28 @@ fn finish_login(result: std::io::Result<()>) -> ! {
async fn run_interactive_login<F>(
cli_config_overrides: CliConfigOverrides,
login: impl FnOnce(Config) -> F,
chatgpt_login: impl FnOnce(Config) -> F,
) -> !
where
F: std::future::Future<Output = std::io::Result<()>>,
{
let config = load_config_or_exit(cli_config_overrides).await;
let mut config = load_config_or_exit(cli_config_overrides.clone()).await;
if !config
.config_layer_stack
.requirements_toml()
.requires_remote_config()
{
let _login_log_guard = init_login_file_logging(&config);
finish_login(chatgpt_login(config).await);
}
let result =
sign_in_to_config_server_and_reload_config(&mut config, &cli_config_overrides).await;
let _login_log_guard = init_login_file_logging(&config);
finish_login(login(config).await)
finish_login(result)
}
pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! {
pub async fn run_login_with_browser(cli_config_overrides: CliConfigOverrides) -> ! {
run_interactive_login(cli_config_overrides, |config| async move {
tracing::info!("starting browser login flow");
@@ -226,7 +258,7 @@ pub async fn run_login_with_api_key(
cli_config_overrides: CliConfigOverrides,
api_key: String,
) -> ! {
let config = load_config_or_exit(cli_config_overrides).await;
let config = load_managed_login_config_or_exit(cli_config_overrides).await;
let _login_log_guard = init_login_file_logging(&config);
tracing::info!("starting api key login flow");
@@ -259,7 +291,7 @@ pub async fn run_login_with_access_token(
cli_config_overrides: CliConfigOverrides,
access_token: String,
) -> ! {
let config = load_config_or_exit(cli_config_overrides).await;
let config = load_managed_login_config_or_exit(cli_config_overrides).await;
let _login_log_guard = init_login_file_logging(&config);
tracing::info!("starting access token login flow");
@@ -521,9 +553,28 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
}
}
async fn logout_config_server(config: &Config) -> std::io::Result<bool> {
match codex_cloud_config::managed_config_auth_manager(
config.auth_config(),
config.config_layer_stack.requirements_toml(),
)? {
Some(manager) => manager.logout().await,
None => Ok(false),
}
}
pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
let config = load_config_or_exit(cli_config_overrides).await;
let auth_route_config = config.auth_route_config();
let config_server_logout = if config
.config_layer_stack
.requirements_toml()
.requires_remote_config()
{
logout_config_server(&config).await
} else {
Ok(false)
};
// Cleanup is independent of the current feature gate. Retain the grant lock
// through account removal so another process cannot commit a late login.
@@ -590,7 +641,14 @@ pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
false
};
if logged_out || cleared_bedrock_config {
let config_server_logged_out = match config_server_logout {
Ok(removed) => removed,
Err(error) => {
eprintln!("Error logging out: {error}");
std::process::exit(1);
}
};
if logged_out || config_server_logged_out || cleared_bedrock_config {
eprintln!("Successfully logged out");
} else {
eprintln!("Not logged in");
@@ -598,6 +656,20 @@ pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
std::process::exit(0);
}
async fn load_managed_login_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
let mut config = load_config_or_exit(cli_config_overrides.clone()).await;
if config
.config_layer_stack
.requirements_toml()
.requires_remote_config()
&& let Err(error) =
sign_in_to_config_server_and_reload_config(&mut config, &cli_config_overrides).await
{
finish_login(Err(error));
}
config
}
async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
let cli_overrides = match cli_config_overrides.parse_overrides() {
Ok(v) => v,
+2 -2
View File
@@ -16,7 +16,7 @@ use codex_cli::read_api_key_from_stdin;
use codex_cli::run_login_status;
use codex_cli::run_login_with_access_token;
use codex_cli::run_login_with_api_key;
use codex_cli::run_login_with_chatgpt;
use codex_cli::run_login_with_browser;
use codex_cli::run_login_with_device_code;
use codex_cli::run_logout;
use codex_cloud_tasks::Cli as CloudTasksCli;
@@ -1699,7 +1699,7 @@ async fn cli_main(
let access_token = read_access_token_from_stdin();
run_login_with_access_token(login_cli.config_overrides, access_token).await;
} else {
run_login_with_chatgpt(login_cli.config_overrides).await;
run_login_with_browser(login_cli.config_overrides).await;
}
}
}
@@ -25,6 +25,27 @@ pub fn managed_config_auth_manager(
crate::config_server_auth_manager(service_config, auth.codex_home, &factory)
}
/// Existing grants are left to the loader's normal cache and refresh handling.
pub async fn ensure_config_server_login(
auth: AuthConfig,
requirements: &ConfigRequirementsToml,
) -> std::io::Result<()> {
let Some(manager) = managed_config_auth_manager(auth, requirements)? else {
return Ok(());
};
if let Err(error) = manager.authorization_id().await {
if error
.get_ref()
.and_then(|source| source.downcast_ref::<codex_login::OAuthSessionError>())
!= Some(&codex_login::OAuthSessionError::LoginRequired)
{
return Err(error);
}
manager.login().await?;
}
Ok(())
}
pub fn managed_config_bundle_loader(
auth_manager: Arc<AuthManager>,
chatgpt_base_url: String,
+1
View File
@@ -16,6 +16,7 @@ mod validation;
pub use bundle_loader::chatgpt_config_bundle_loader;
pub use bundle_loader::chatgpt_config_bundle_loader_for_storage;
pub use bundle_loader::chatgpt_config_bundle_loader_for_storage_without_cache;
pub use bundle_loader::ensure_config_server_login;
pub use bundle_loader::managed_config_auth_manager;
pub use bundle_loader::managed_config_bundle_loader;
pub use bundle_loader::managed_config_bundle_loader_for_storage;
+5 -3
View File
@@ -58,7 +58,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_config::chatgpt_config_bundle_loader_for_storage;
use codex_cloud_config::managed_config_bundle_loader_for_storage;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLoadError;
use codex_config::ConfigLoadOptions;
@@ -407,9 +407,10 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
CloudConfigBundleLoader::default(),
)
.await;
let gate_cloud_config = chatgpt_config_bundle_loader_for_storage(
let gate_cloud_config = managed_config_bundle_loader_for_storage(
embedded_network_policy
.bind_bootstrap_auth(bootstrap_auth_config(&codex_home, &gate_bootstrap)?),
gate_bootstrap.config_layer_stack.requirements_toml(),
/*enable_codex_api_key_env*/ false,
)
.await?;
@@ -507,8 +508,9 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
// API keys cannot fetch workspace-managed configuration. Preserve the
// existing ChatGPT bootstrap identity even when model requests allow
// CODEX_API_KEY.
let cloud_config_bundle = chatgpt_config_bundle_loader_for_storage(
let cloud_config_bundle = managed_config_bundle_loader_for_storage(
bootstrap_auth_config,
bootstrap_config.config_layer_stack.requirements_toml(),
/*enable_codex_api_key_env*/ false,
)
.await?;
+5 -3
View File
@@ -47,7 +47,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_config::chatgpt_config_bundle_loader_for_storage;
use codex_cloud_config::managed_config_bundle_loader_for_storage;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLoadError;
use codex_config::LoaderOverrides;
@@ -1098,11 +1098,12 @@ async fn cloud_config_bundle_for_app_server_target(
codex_home: &Path,
embedded_network_policy: &codex_app_server_client::EmbeddedNetworkPolicy,
) -> std::io::Result<CloudConfigBundleLoader> {
chatgpt_config_bundle_loader_for_storage(
managed_config_bundle_loader_for_storage(
embedded_network_policy
.bind_bootstrap_auth(app_server_target.auth_config_for_cloud_loader(
bootstrap_auth_config(codex_home, bootstrap_config)?,
)),
bootstrap_config.config_layer_stack.requirements_toml(),
/*enable_codex_api_key_env*/ false,
)
.await
@@ -1415,8 +1416,9 @@ async fn run_ratatui_app(
// bundle and rebuild config. This avoids missing newly available cloud-managed
// policy due to login status detection edge cases.
if show_login_screen && !uses_remote_workspace && !workload_identity_selected {
cloud_config_bundle = chatgpt_config_bundle_loader_for_storage(
cloud_config_bundle = managed_config_bundle_loader_for_storage(
embedded_network_policy.bind_bootstrap_auth(initial_config.auth_config()),
initial_config.config_layer_stack.requirements_toml(),
/*enable_codex_api_key_env*/ false,
)
.await?;
+15 -1
View File
@@ -305,6 +305,19 @@ pub(super) async fn run_main_inner(
err
}
})?;
if std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
codex_cloud_config::ensure_config_server_login(
embedded_network_policy.bind_bootstrap_auth(bootstrap_auth_config(
&codex_home,
&presentation.bootstrap_config,
)?),
presentation
.bootstrap_config
.config_layer_stack
.requirements_toml(),
)
.await?;
}
// Keep normal terminal signals available until local configuration is ready. Once raw
// mode begins, StartupDraft immediately takes ownership of input.
let (initialized_terminal, terminal_restore_guard) = tokio::task::spawn_blocking(|| {
@@ -508,10 +521,11 @@ pub(super) async fn run_main_inner(
cloud_config_bundle
} else {
startup_draft
.run_until(chatgpt_config_bundle_loader_for_storage(
.run_until(managed_config_bundle_loader_for_storage(
embedded_network_policy.bind_bootstrap_auth(
app_server_target.auth_config_for_cloud_loader(config.auth_config()),
),
config.config_layer_stack.requirements_toml(),
/*enable_codex_api_key_env*/ false,
))
.await??