mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Honor cloud-managed requirements in feature listings (#38581)
## What changed - Load configuration for `codex features list` through the shared cloud-aware loader so managed feature requirements are reflected in the reported state. - Reuse that loader for MCP commands. ## Testing - Add an integration test showing that a cloud-managed requirement can disable `fast_mode` in `codex features list` without rewriting the user's `config.toml`. GitOrigin-RevId: 2bdbc76dfa40ceb29e8293556f4b56cabbd2c43f
This commit is contained in:
1 parent
2a452d7dc1
commit
7c194ff24b
4 files changed
+114
-23
No files matched your search
@@ -12,7 +12,7 @@ use codex_core::config::load_config_toml_with_layer_stack;
|
|||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use codex_utils_cli::CliConfigOverrides;
|
use codex_utils_cli::CliConfigOverrides;
|
||||||
|
|
||||||
pub(super) async fn load_mcp_config(
|
pub(crate) async fn load_config(
|
||||||
config_overrides: &CliConfigOverrides,
|
config_overrides: &CliConfigOverrides,
|
||||||
loader_overrides: LoaderOverrides,
|
loader_overrides: LoaderOverrides,
|
||||||
) -> Result<Config> {
|
) -> Result<Config> {
|
||||||
@@ -45,6 +45,7 @@ use supports_color::Stream;
|
|||||||
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||||
mod app_cmd;
|
mod app_cmd;
|
||||||
|
mod cloud_config;
|
||||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||||
mod desktop_app;
|
mod desktop_app;
|
||||||
mod doctor;
|
mod doctor;
|
||||||
@@ -1640,22 +1641,16 @@ async fn cli_main(
|
|||||||
root_remote_auth_token_env.as_deref(),
|
root_remote_auth_token_env.as_deref(),
|
||||||
"features list",
|
"features list",
|
||||||
)?;
|
)?;
|
||||||
let mut cli_kv_overrides = root_config_overrides
|
|
||||||
.parse_overrides()
|
|
||||||
.map_err(anyhow::Error::msg)?;
|
|
||||||
|
|
||||||
// Honor `--search` via the canonical web_search mode.
|
// Honor `--search` via the canonical web_search mode.
|
||||||
if interactive.web_search {
|
if interactive.web_search {
|
||||||
cli_kv_overrides.push((
|
root_config_overrides
|
||||||
"web_search".to_string(),
|
.raw_overrides
|
||||||
toml::Value::String("live".to_string()),
|
.push("web_search=\"live\"".to_string());
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = ConfigBuilder::default()
|
let config =
|
||||||
.cli_overrides(cli_kv_overrides)
|
cloud_config::load_config(&root_config_overrides, LoaderOverrides::default())
|
||||||
.build()
|
.await?;
|
||||||
.await?;
|
|
||||||
let mut rows = Vec::with_capacity(FEATURES.len());
|
let mut rows = Vec::with_capacity(FEATURES.len());
|
||||||
let mut name_width = 0;
|
let mut name_width = 0;
|
||||||
let mut stage_width = 0;
|
let mut stage_width = 0;
|
||||||
|
|||||||
@@ -40,10 +40,9 @@ use codex_rmcp_client::perform_oauth_login;
|
|||||||
use codex_utils_cli::CliConfigOverrides;
|
use codex_utils_cli::CliConfigOverrides;
|
||||||
use codex_utils_cli::format_env_display;
|
use codex_utils_cli::format_env_display;
|
||||||
|
|
||||||
|
use crate::cloud_config;
|
||||||
use crate::plugin_cmd::load_cli_auth_mode;
|
use crate::plugin_cmd::load_cli_auth_mode;
|
||||||
|
|
||||||
mod cloud_config;
|
|
||||||
|
|
||||||
/// Subcommands:
|
/// Subcommands:
|
||||||
/// - `list` — list configured servers (with `--json`)
|
/// - `list` — list configured servers (with `--json`)
|
||||||
/// - `get` — show a single server (with `--json`)
|
/// - `get` — show a single server (with `--json`)
|
||||||
@@ -227,13 +226,11 @@ impl McpCli {
|
|||||||
|
|
||||||
match subcommand {
|
match subcommand {
|
||||||
McpSubcommand::List(args) => {
|
McpSubcommand::List(args) => {
|
||||||
let config =
|
let config = cloud_config::load_config(&config_overrides, loader_overrides).await?;
|
||||||
cloud_config::load_mcp_config(&config_overrides, loader_overrides).await?;
|
|
||||||
run_list(&config, args).await?;
|
run_list(&config, args).await?;
|
||||||
}
|
}
|
||||||
McpSubcommand::Get(args) => {
|
McpSubcommand::Get(args) => {
|
||||||
let config =
|
let config = cloud_config::load_config(&config_overrides, loader_overrides).await?;
|
||||||
cloud_config::load_mcp_config(&config_overrides, loader_overrides).await?;
|
|
||||||
run_get(&config, args).await?;
|
run_get(&config, args).await?;
|
||||||
}
|
}
|
||||||
McpSubcommand::Add(args) => {
|
McpSubcommand::Add(args) => {
|
||||||
@@ -243,13 +240,11 @@ impl McpCli {
|
|||||||
run_remove(&config_overrides, args).await?;
|
run_remove(&config_overrides, args).await?;
|
||||||
}
|
}
|
||||||
McpSubcommand::Login(args) => {
|
McpSubcommand::Login(args) => {
|
||||||
let config =
|
let config = cloud_config::load_config(&config_overrides, loader_overrides).await?;
|
||||||
cloud_config::load_mcp_config(&config_overrides, loader_overrides).await?;
|
|
||||||
run_login(&config, args).await?;
|
run_login(&config, args).await?;
|
||||||
}
|
}
|
||||||
McpSubcommand::Logout(args) => {
|
McpSubcommand::Logout(args) => {
|
||||||
let config =
|
let config = cloud_config::load_config(&config_overrides, loader_overrides).await?;
|
||||||
cloud_config::load_mcp_config(&config_overrides, loader_overrides).await?;
|
|
||||||
run_logout(&config, args).await?;
|
run_logout(&config, args).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use app_test_support::ChatGptAuthFixture;
|
||||||
|
use app_test_support::write_chatgpt_auth;
|
||||||
|
use codex_config::ConfigLoadOptions;
|
||||||
|
use codex_config::types::AuthCredentialsStoreMode;
|
||||||
|
use codex_core::config::load_config_toml_with_layer_stack;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use predicates::str::contains;
|
use predicates::str::contains;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
use wiremock::Mock;
|
||||||
|
use wiremock::MockServer;
|
||||||
|
use wiremock::ResponseTemplate;
|
||||||
|
use wiremock::matchers::header;
|
||||||
|
use wiremock::matchers::method;
|
||||||
|
use wiremock::matchers::path;
|
||||||
|
|
||||||
fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {
|
fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {
|
||||||
let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
|
let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
|
||||||
@@ -117,3 +131,90 @@ async fn features_list_is_sorted_alphabetically_by_feature_name() -> Result<()>
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn features_list_honors_cloud_managed_feature_requirements() -> Result<()> {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
let chatgpt_base_url = format!("{}/backend-api", server.uri());
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let user_config = format!(
|
||||||
|
"cli_auth_credentials_store = \"file\"\nchatgpt_base_url = \"{chatgpt_base_url}\"\n\n[features]\nfast_mode = true\n"
|
||||||
|
);
|
||||||
|
std::fs::write(codex_home.path().join("config.toml"), &user_config)?;
|
||||||
|
|
||||||
|
let bootstrap_config = load_config_toml_with_layer_stack(
|
||||||
|
codex_home.path(),
|
||||||
|
Some(&AbsolutePathBuf::from_absolute_path(codex_home.path())?),
|
||||||
|
Vec::new(),
|
||||||
|
ConfigLoadOptions::default(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if bootstrap_config.config_toml.cli_auth_credentials_store
|
||||||
|
!= Some(AuthCredentialsStoreMode::File)
|
||||||
|
|| bootstrap_config.config_toml.chatgpt_base_url.as_deref()
|
||||||
|
!= Some(chatgpt_base_url.as_str())
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"skipping cloud-managed feature subprocess: host-managed authentication or backend routing prevents isolated mock credentials"
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
write_chatgpt_auth(
|
||||||
|
codex_home.path(),
|
||||||
|
ChatGptAuthFixture::new("chatgpt-token")
|
||||||
|
.account_id("workspace-123")
|
||||||
|
.chatgpt_account_id("workspace-123")
|
||||||
|
.chatgpt_user_id("user-123")
|
||||||
|
.plan_type("enterprise"),
|
||||||
|
AuthCredentialsStoreMode::File,
|
||||||
|
)?;
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/backend-api/wham/config/bundle"))
|
||||||
|
.and(header("authorization", "Bearer chatgpt-token"))
|
||||||
|
.and(header("chatgpt-account-id", "workspace-123"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"requirements_toml": {
|
||||||
|
"enterprise_managed": [{
|
||||||
|
"id": "managed-feature-requirements",
|
||||||
|
"name": "Managed feature requirements",
|
||||||
|
"contents": "[features]\nfast_mode = false\n",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
})))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut cmd = codex_command(codex_home.path())?;
|
||||||
|
let output = cmd
|
||||||
|
.current_dir(codex_home.path())
|
||||||
|
.env("NO_PROXY", "127.0.0.1,localhost")
|
||||||
|
.env("no_proxy", "127.0.0.1,localhost")
|
||||||
|
.env_remove("CODEX_ACCESS_TOKEN")
|
||||||
|
.env_remove("CODEX_API_KEY")
|
||||||
|
.env_remove("OPENAI_API_KEY")
|
||||||
|
.args(["features", "list"])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.get_output()
|
||||||
|
.stdout
|
||||||
|
.clone();
|
||||||
|
let stdout = String::from_utf8(output)?;
|
||||||
|
let fast_mode = stdout
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.starts_with("fast_mode "))
|
||||||
|
.context("feature list should include fast_mode")?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fast_mode.split_whitespace().collect::<Vec<_>>(),
|
||||||
|
["fast_mode", "stable", "false"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(codex_home.path().join("config.toml"))?,
|
||||||
|
user_config
|
||||||
|
);
|
||||||
|
server.verify().await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in new issue
Block a user