mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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??
|
||||
|
||||
Reference in New Issue
Block a user