Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions codex-rs/app-server/src/config_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,21 +192,38 @@ impl ConfigManager {
&self,
fallback_cwd: Option<PathBuf>,
) -> std::io::Result<Config> {
let Some(fallback_cwd) = fallback_cwd else {
return self.load_non_project_config().await;
};
self.load_with_cli_overrides(
&self.current_cli_overrides(),
/*request_overrides*/ None,
ConfigOverrides::default(),
fallback_cwd,
Some(fallback_cwd),
)
.await
}

/// Loads system, user, and runtime settings without discovering a project
/// from the app-server process's working directory.
pub(crate) async fn load_non_project_config(&self) -> std::io::Result<Config> {
let mut manager = self.clone();
manager.loader_overrides.ignore_project_config = true;
manager.load_latest_config(/*fallback_cwd*/ None).await
let policy_load = self.refresh_application_network_policy().await?;
let mut config = ConfigBuilder::default()
.codex_home(self.codex_home.clone())
.cli_overrides(self.current_cli_overrides())
.loader_overrides(self.loader_overrides.clone())
.strict_config(self.strict_config)
.fallback_cwd(Some(self.codex_home.clone()))
.cloud_config_bundle(policy_load.cloud_config.clone())
.thread_config_loader(Arc::clone(&self.thread_config_loader))
.without_project_context()
.build()
.await?;
self.check_application_policy_load(&policy_load)?;
self.apply_network_policy(&mut config);
self.apply_runtime_feature_enablement(&mut config);
self.apply_arg0_paths(&mut config);
Ok(config)
}

pub(crate) async fn load_latest_config_with_session_layers(
Expand Down
8 changes: 3 additions & 5 deletions codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ pub async fn run_main_with_transport_options(
)
})?;
let codex_home = find_codex_home()?;
let startup_cwd = std::env::current_dir().ok();
let local_runtime_paths = ExecServerRuntimeOptions::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
Expand All @@ -544,7 +545,7 @@ pub async fn run_main_with_transport_options(
Arc::new(NoopThreadConfigLoader),
);
let bootstrap_config = config_manager
.load_startup_config(/*fallback_cwd*/ None)
.load_startup_config(startup_cwd.clone())
.await?;
let bootstrap_auth =
AuthManager::shared_from_config(&bootstrap_config, /*enable_codex_api_key_env*/ false)
Expand All @@ -557,10 +558,7 @@ pub async fn run_main_with_transport_options(
);
let mut config_warnings = Vec::new();
let mut plugin_startup_config = PluginStartupConfig::Current;
let config = match config_manager
.load_latest_config(/*fallback_cwd*/ None)
.await
{
let config = match config_manager.load_latest_config(startup_cwd).await {
Ok(config) => config,
Err(err) if is_unsupported_untrusted_approval_policy_error(&err) => {
return Err(err);
Expand Down
7 changes: 2 additions & 5 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,11 +465,8 @@ impl MessageProcessor {
rpc_transport,
Arc::clone(&user_verification),
);
let marketplace_processor = MarketplaceRequestProcessor::new(
Arc::clone(&config),
config_manager.clone(),
Arc::clone(&thread_manager),
);
let marketplace_processor =
MarketplaceRequestProcessor::new(config_manager.clone(), Arc::clone(&thread_manager));
let mcp_processor = McpRequestProcessor::new(
auth_manager.clone(),
Arc::clone(&thread_manager),
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/app-server/src/plugin_config_reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ pub(crate) fn for_cwd(manager: ConfigManager, cwd: AbsolutePathBuf) -> ConfigLay
Arc::new(move || runtime.block_on(manager.load_config_layers_for_cwd(cwd.clone())))
}

/// Reloads config without deriving project context from a process cwd.
pub(crate) fn global(manager: ConfigManager) -> ConfigLayerReload {
let runtime = tokio::runtime::Handle::current();
Arc::new(move || {
runtime
.block_on(manager.load_non_project_config())
.map(|config| config.config_layer_stack)
})
}

pub(crate) fn defaults(manager: ConfigManager) -> ConfigLayerReload {
let runtime = tokio::runtime::Handle::current();
Arc::new(move || {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,13 @@ use crate::plugin_config_reload;

#[derive(Clone)]
pub(crate) struct MarketplaceRequestProcessor {
config: Arc<Config>,
config_manager: ConfigManager,
thread_manager: Arc<ThreadManager>,
}

impl MarketplaceRequestProcessor {
pub(crate) fn new(
config: Arc<Config>,
config_manager: ConfigManager,
thread_manager: Arc<ThreadManager>,
) -> Self {
pub(crate) fn new(config_manager: ConfigManager, thread_manager: Arc<ThreadManager>) -> Self {
Self {
config,
config_manager,
thread_manager,
}
Expand Down Expand Up @@ -52,7 +46,7 @@ impl MarketplaceRequestProcessor {
&self,
params: MarketplaceRemoveParams,
) -> Result<MarketplaceRemoveResponse, JSONRPCErrorError> {
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let config = self.load_global_config().await?;
remove_marketplace(
config.codex_home.to_path_buf(),
config.config_layer_stack,
Expand All @@ -75,12 +69,11 @@ impl MarketplaceRequestProcessor {
&self,
params: MarketplaceUpgradeParams,
) -> Result<MarketplaceUpgradeResponse, JSONRPCErrorError> {
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let config = self.load_global_config().await?;
let plugins_manager = self.thread_manager.plugins_manager();
let MarketplaceUpgradeParams { marketplace_name } = params;
let plugins_input = config.plugins_config_input();
let reload_config =
plugin_config_reload::for_cwd(self.config_manager.clone(), config.cwd.clone());
let reload_config = plugin_config_reload::global(self.config_manager.clone());

let outcome = tokio::task::spawn_blocking(move || {
plugins_manager.upgrade_configured_marketplaces_for_config(
Expand Down Expand Up @@ -118,9 +111,9 @@ impl MarketplaceRequestProcessor {
&self,
params: MarketplaceAddParams,
) -> Result<MarketplaceAddResponse, JSONRPCErrorError> {
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let config = self.load_global_config().await?;
add_marketplace_to_codex_home(
self.config.codex_home.to_path_buf(),
config.codex_home.to_path_buf(),
config.config_layer_stack.requirements().clone(),
MarketplaceAddRequest {
source: params.source,
Expand All @@ -140,12 +133,9 @@ impl MarketplaceRequestProcessor {
})
}

async fn load_latest_config(
&self,
fallback_cwd: Option<PathBuf>,
) -> Result<Config, JSONRPCErrorError> {
async fn load_global_config(&self) -> Result<Config, JSONRPCErrorError> {
self.config_manager
.load_latest_config(fallback_cwd)
.load_non_project_config()
.await
.map_err(|err| internal_error(format!("failed to reload config: {err}")))
}
Expand Down
13 changes: 5 additions & 8 deletions codex-rs/app-server/src/request_processors/mcp_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,9 @@ impl McpRequestProcessor {
Ok(McpServerRefreshResponse {})
}

async fn load_latest_config(
&self,
fallback_cwd: Option<PathBuf>,
) -> Result<Config, JSONRPCErrorError> {
async fn load_global_config(&self) -> Result<Config, JSONRPCErrorError> {
self.config_manager
.load_latest_config(fallback_cwd)
.load_non_project_config()
.await
.map_err(|err| internal_error(format!("failed to reload config: {err}")))
}
Expand Down Expand Up @@ -152,7 +149,7 @@ impl McpRequestProcessor {
((*config).clone(), runtime_context)
}
None => {
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let config = self.load_global_config().await?;
let mcp_config = self
.thread_manager
.mcp_manager()
Expand Down Expand Up @@ -421,7 +418,7 @@ impl McpRequestProcessor {
.await
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?
}
None => self.load_latest_config(/*fallback_cwd*/ None).await?,
None => self.load_global_config().await?,
};
let mcp_manager = self.thread_manager.mcp_manager();
let auth = self.auth_manager.auth().await;
Expand Down Expand Up @@ -622,7 +619,7 @@ impl McpRequestProcessor {
return Err(invalid_request("originCallId requires threadId"));
}

let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let config = self.load_global_config().await?;
let mcp_manager = self.thread_manager.mcp_manager();
let mcp_config = mcp_manager.runtime_config(&config).await;
let codex_apps_tools_cache = mcp_manager.codex_apps_tools_cache();
Expand Down
36 changes: 35 additions & 1 deletion codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,16 @@ async fn experimental_feature_list_resolves_thread_project_config() -> Result<()
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
let workspace = TempDir::new()?;
let codex_home_key = codex_home.path().to_string_lossy().replace('\\', "\\\\");
let workspace_key = workspace.path().to_string_lossy().replace('\\', "\\\\");
MockResponsesConfig::new(&server.uri())
.with_extra_config(&format!(
"[projects.\"{workspace_key}\"]\ntrust_level = \"trusted\""
"[projects.\"{codex_home_key}\"]\ntrust_level = \"trusted\"\n\
[projects.\"{workspace_key}\"]\ntrust_level = \"trusted\""
))
.write(codex_home.path())?;
#[cfg(unix)]
let config_toml = std::fs::read(codex_home.path().join("config.toml"))?;
let project_config_dir = workspace.path().join(".codex");
std::fs::create_dir_all(&project_config_dir)?;
std::fs::write(
Expand All @@ -159,13 +163,43 @@ async fn experimental_feature_list_resolves_thread_project_config() -> Result<()
memories = true
"#,
)?;
let launch_project_config_dir = codex_home.path().join(".codex");
std::fs::create_dir(&launch_project_config_dir)?;
std::fs::copy(
project_config_dir.join("config.toml"),
launch_project_config_dir.join("config.toml"),
)?;

let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_managed_config()
.build_initialized_with_timeout(DEFAULT_TIMEOUT)
.await?;

#[cfg(unix)]
{
let deleted_launch_dir = workspace.path().join("deleted-launch");
std::fs::rename(codex_home.path(), &deleted_launch_dir)?;
std::fs::create_dir(codex_home.path())?;
std::fs::write(codex_home.path().join("config.toml"), config_toml)?;
std::fs::create_dir(&launch_project_config_dir)?;
std::fs::copy(
project_config_dir.join("config.toml"),
launch_project_config_dir.join("config.toml"),
)?;
std::fs::remove_dir_all(deleted_launch_dir)?;
}
let request_id = mcp
.send_experimental_feature_list_request(ExperimentalFeatureListParams::default())
.await?;
let global = read_response::<ExperimentalFeatureListResponse>(&mut mcp, request_id).await?;
let global_memories = global
.data
.iter()
.find(|feature| feature.name == "memories")
.expect("memories feature should be present");
assert!(!global_memories.enabled);

let thread_start_id = mcp
.send_thread_start_request_with_auto_env(ThreadStartParams {
cwd: Some(workspace.path().display().to_string()),
Expand Down
37 changes: 12 additions & 25 deletions codex-rs/app-server/tests/suite/v2/marketplace_remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,11 @@ async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> {
Ok(())
}

#[test_case(false; "project only")]
#[test_case(true; "project and user")]
#[tokio::test]
async fn marketplace_remove_preserves_project_marketplace(user_entry: bool) -> Result<()> {
async fn marketplace_remove_ignores_startup_project_config() -> Result<()> {
let codex_home = TempDir::new()?;
if user_entry {
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
}
// TestAppServer starts in CODEX_HOME, so make it a trusted project as well.
// TestAppServer starts in CODEX_HOME, so put project config there to verify
// that the global marketplace RPC does not use it.
std::fs::create_dir_all(codex_home.path().join(".git"))?;
std::fs::create_dir_all(codex_home.path().join(".codex"))?;
let project_config_path = codex_home.path().join(".codex/config.toml");
Expand All @@ -141,35 +137,26 @@ async fn marketplace_remove_preserves_project_marketplace(user_entry: bool) -> R
write_installed_marketplace(codex_home.path(), "debug")?;
let snapshot_path =
marketplace_install_root(codex_home.path()).join("debug/.agents/plugins/marketplace.json");
let user_config_path = codex_home.path().join("config.toml");

let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized()
.await?;
let user_config = std::fs::read_to_string(&user_config_path)?;
let request_id = mcp
.send_marketplace_remove_request(MarketplaceRemoveParams {
marketplace_name: "debug".to_string(),
let response: MarketplaceRemoveResponse = mcp
.request(|request_id| ClientRequest::MarketplaceRemove {
request_id,
params: MarketplaceRemoveParams {
marketplace_name: "debug".to_string(),
},
})
.await?;
let err = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;

assert_eq!(err.error.code, -32600);
assert!(
err.error
.message
.starts_with("marketplace `debug` is configured in project (")
);
assert_eq!(std::fs::read_to_string(user_config_path)?, user_config);
assert_eq!(response.marketplace_name, "debug");
assert!(response.installed_root.is_some());
assert_eq!(
std::fs::read_to_string(project_config_path)?,
project_config
);
assert_eq!(std::fs::read_to_string(snapshot_path)?, "{}");
assert!(!snapshot_path.exists());
Ok(())
}
10 changes: 9 additions & 1 deletion codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,7 @@ pub struct ConfigBuilder {
cloud_config_bundle: CloudConfigBundleLoader,
thread_config_loader: Option<Arc<dyn ThreadConfigLoader>>,
fallback_cwd: Option<PathBuf>,
without_project_context: bool,
}

impl ConfigBuilder {
Expand Down Expand Up @@ -1503,6 +1504,12 @@ impl ConfigBuilder {
self
}

/// Materializes `Config.cwd` without using it as config-layer context.
pub fn without_project_context(mut self) -> Self {
self.without_project_context = true;
self
}

pub async fn build(self) -> std::io::Result<Config> {
// Keep the large config-loading future off small runtime thread stacks.
Box::pin(self.build_inner()).await
Expand All @@ -1518,6 +1525,7 @@ impl ConfigBuilder {
cloud_config_bundle,
thread_config_loader,
fallback_cwd,
without_project_context,
} = self;
let codex_home = match codex_home {
Some(codex_home) => AbsolutePathBuf::from_absolute_path(codex_home)?,
Expand All @@ -1535,7 +1543,7 @@ impl ConfigBuilder {
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
(!without_project_context).then_some(cwd),
&cli_overrides,
ConfigLoadOptions {
loader_overrides,
Expand Down
Loading