mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
core: reload override config at runtime
This commit is contained in:
@@ -7,6 +7,7 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_OVERRIDE_TOML_FILE;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::ConfigLayerStack;
|
||||
@@ -81,25 +82,49 @@ async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec<LayerMtime
|
||||
}
|
||||
|
||||
fn collect_layer_mtimes(stack: &ConfigLayerStack) -> Vec<LayerMtime> {
|
||||
stack
|
||||
.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
)
|
||||
let layers = stack.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
);
|
||||
let mut layer_mtimes = layers
|
||||
.iter()
|
||||
.filter_map(|layer| {
|
||||
let path = match &layer.name {
|
||||
ConfigLayerSource::System { file } => Some(file.clone()),
|
||||
ConfigLayerSource::User { file, .. } => Some(file.clone()),
|
||||
ConfigLayerSource::System { file } | ConfigLayerSource::SystemOverride { file } => {
|
||||
Some(file.clone())
|
||||
}
|
||||
ConfigLayerSource::User { file, .. } | ConfigLayerSource::UserOverride { file } => {
|
||||
Some(file.clone())
|
||||
}
|
||||
ConfigLayerSource::Project { dot_codex_folder } => {
|
||||
Some(dot_codex_folder.join(CONFIG_TOML_FILE))
|
||||
}
|
||||
ConfigLayerSource::ProjectOverride { dot_codex_folder } => {
|
||||
Some(dot_codex_folder.join(CONFIG_OVERRIDE_TOML_FILE))
|
||||
}
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.clone()),
|
||||
_ => None,
|
||||
};
|
||||
path.map(LayerMtime::new)
|
||||
})
|
||||
.collect()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let watched_paths = layer_mtimes
|
||||
.iter()
|
||||
.map(|layer| layer.path.clone())
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
let optional_override_paths = layers
|
||||
.into_iter()
|
||||
.filter_map(|layer| match &layer.name {
|
||||
ConfigLayerSource::System { file } | ConfigLayerSource::User { file, .. } => file
|
||||
.parent()
|
||||
.map(|parent| parent.join(CONFIG_OVERRIDE_TOML_FILE)),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|path| !watched_paths.contains(path))
|
||||
.map(LayerMtime::new);
|
||||
layer_mtimes.extend(optional_override_paths);
|
||||
layer_mtimes
|
||||
}
|
||||
|
||||
fn enforce_trusted_constraints(
|
||||
@@ -256,7 +281,9 @@ fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool {
|
||||
matches!(
|
||||
layer,
|
||||
ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::UserOverride { .. }
|
||||
| ConfigLayerSource::Project { .. }
|
||||
| ConfigLayerSource::ProjectOverride { .. }
|
||||
| ConfigLayerSource::SessionFlags
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,36 @@ use super::*;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::NetworkRuleProtocol;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn network_proxy_reloader_notices_new_user_override_files() {
|
||||
let temp = tempdir().expect("create temp dir");
|
||||
let base_file =
|
||||
AbsolutePathBuf::try_from(temp.path().join(CONFIG_TOML_FILE)).expect("base path");
|
||||
let override_file = AbsolutePathBuf::try_from(temp.path().join(CONFIG_OVERRIDE_TOML_FILE))
|
||||
.expect("override path");
|
||||
std::fs::write(base_file.as_path(), "").expect("write base config");
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![codex_config::ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User {
|
||||
file: base_file,
|
||||
profile: None,
|
||||
},
|
||||
toml::Value::Table(Default::default()),
|
||||
)],
|
||||
codex_config::ConfigRequirements::default(),
|
||||
codex_config::ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("stack");
|
||||
let reloader = MtimeConfigReloader::new(collect_layer_mtimes(&stack));
|
||||
|
||||
assert!(!reloader.needs_reload().await);
|
||||
std::fs::write(override_file.as_path(), "[network_proxy]\n").expect("write override");
|
||||
assert!(reloader.needs_reload().await);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_profile_network_overlays_domain_entries() {
|
||||
|
||||
@@ -180,6 +180,7 @@ use crate::config::resolve_web_search_mode_for_turn;
|
||||
use crate::context_manager::ContextManager;
|
||||
use crate::context_manager::TotalTokenUsageBreakdown;
|
||||
use crate::thread_rollout_truncation::initial_history_has_prior_user_turns;
|
||||
use codex_config::CONFIG_OVERRIDE_TOML_FILE;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
@@ -1512,6 +1513,11 @@ impl Session {
|
||||
//
|
||||
// Prefer `refresh_runtime_config()` when the host can already provide a materialized
|
||||
// config snapshot. This file-based path exists for legacy local reload flows.
|
||||
enum UserConfigReloadLayer {
|
||||
Base(AbsolutePathBuf),
|
||||
Override(AbsolutePathBuf),
|
||||
}
|
||||
|
||||
let config_toml_paths = {
|
||||
let state = self.state.lock().await;
|
||||
let config = &state.session_configuration.original_config_do_not_use;
|
||||
@@ -1523,25 +1529,47 @@ impl Session {
|
||||
)
|
||||
.into_iter()
|
||||
.filter_map(|layer| match &layer.name {
|
||||
ConfigLayerSource::User { file, .. } => Some(file.clone()),
|
||||
ConfigLayerSource::User { file, .. } => {
|
||||
Some(UserConfigReloadLayer::Base(file.clone()))
|
||||
}
|
||||
ConfigLayerSource::UserOverride { file } => {
|
||||
Some(UserConfigReloadLayer::Override(file.clone()))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let override_path = state
|
||||
.session_configuration
|
||||
.codex_home
|
||||
.join(CONFIG_OVERRIDE_TOML_FILE);
|
||||
let has_override_path = user_config_paths.iter().any(|layer| {
|
||||
matches!(
|
||||
layer,
|
||||
UserConfigReloadLayer::Override(path) if path == &override_path
|
||||
)
|
||||
});
|
||||
let mut user_config_paths = user_config_paths;
|
||||
if !has_override_path && override_path.as_path().exists() {
|
||||
user_config_paths.push(UserConfigReloadLayer::Override(override_path));
|
||||
}
|
||||
if user_config_paths.is_empty() {
|
||||
vec![
|
||||
vec![UserConfigReloadLayer::Base(
|
||||
state
|
||||
.session_configuration
|
||||
.codex_home
|
||||
.join(CONFIG_TOML_FILE),
|
||||
]
|
||||
)]
|
||||
} else {
|
||||
user_config_paths
|
||||
}
|
||||
};
|
||||
|
||||
let mut reloaded_user_configs = Vec::with_capacity(config_toml_paths.len());
|
||||
for config_toml_path in config_toml_paths {
|
||||
let user_config = match std::fs::read_to_string(&config_toml_path) {
|
||||
for reload_layer in config_toml_paths {
|
||||
let config_toml_path = match &reload_layer {
|
||||
UserConfigReloadLayer::Base(path) | UserConfigReloadLayer::Override(path) => path,
|
||||
};
|
||||
let user_config = match std::fs::read_to_string(config_toml_path.as_path()) {
|
||||
Ok(contents) => match toml::from_str::<toml::Value>(&contents) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
@@ -1557,16 +1585,21 @@ impl Session {
|
||||
return;
|
||||
}
|
||||
};
|
||||
reloaded_user_configs.push((config_toml_path, user_config));
|
||||
reloaded_user_configs.push((reload_layer, user_config));
|
||||
}
|
||||
|
||||
let next_config = {
|
||||
let state = self.state.lock().await;
|
||||
let mut config = (*state.session_configuration.original_config_do_not_use).clone();
|
||||
for (config_toml_path, user_config) in reloaded_user_configs {
|
||||
config.config_layer_stack = config
|
||||
.config_layer_stack
|
||||
.with_user_config(&config_toml_path, user_config);
|
||||
for (reload_layer, user_config) in reloaded_user_configs {
|
||||
config.config_layer_stack = match reload_layer {
|
||||
UserConfigReloadLayer::Base(config_toml_path) => config
|
||||
.config_layer_stack
|
||||
.with_user_config(&config_toml_path, user_config),
|
||||
UserConfigReloadLayer::Override(config_toml_path) => config
|
||||
.config_layer_stack
|
||||
.with_user_override_config(&config_toml_path, user_config),
|
||||
};
|
||||
}
|
||||
config.tool_suggest =
|
||||
resolve_tool_suggest_config_from_layer_stack(&config.config_layer_stack);
|
||||
|
||||
@@ -1269,6 +1269,112 @@ async fn reload_user_config_layer_updates_base_and_selected_profile_layers() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reload_user_config_layer_refreshes_user_override_layers() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let codex_home = session.codex_home().await;
|
||||
std::fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
let config_toml_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let override_path = codex_home.join(codex_config::CONFIG_OVERRIDE_TOML_FILE);
|
||||
std::fs::write(&config_toml_path, "model = \"base\"\n").expect("write base config");
|
||||
std::fs::write(&override_path, "model = \"override-old\"\n").expect("write override");
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("load override config");
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
}
|
||||
std::fs::write(&override_path, "model = \"override-new\"\n").expect("update override");
|
||||
|
||||
session.reload_user_config_layer().await;
|
||||
|
||||
let config = session.get_config().await;
|
||||
assert_eq!(
|
||||
config
|
||||
.config_layer_stack
|
||||
.effective_user_config()
|
||||
.expect("merged user config")
|
||||
.get("model")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("override-new")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reload_user_config_layer_discovers_new_user_override_layers() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let codex_home = session.codex_home().await;
|
||||
std::fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
let config_toml_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let override_path = codex_home.join(codex_config::CONFIG_OVERRIDE_TOML_FILE);
|
||||
std::fs::write(&config_toml_path, "model = \"base\"\n").expect("write base config");
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("load base config");
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
}
|
||||
std::fs::write(&override_path, "model = \"override-new\"\n").expect("write override");
|
||||
|
||||
session.reload_user_config_layer().await;
|
||||
|
||||
let config = session.get_config().await;
|
||||
assert_eq!(
|
||||
config
|
||||
.config_layer_stack
|
||||
.effective_user_config()
|
||||
.expect("merged user config")
|
||||
.get("model")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("override-new")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reload_user_config_layer_keeps_selected_non_profile_user_above_new_override() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let codex_home = session.codex_home().await;
|
||||
std::fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
let base_config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let override_path = codex_home.join(codex_config::CONFIG_OVERRIDE_TOML_FILE);
|
||||
let selected_config_path = codex_home.join("selected.toml");
|
||||
std::fs::write(&base_config_path, "model = \"base\"\n").expect("write base config");
|
||||
std::fs::write(&selected_config_path, "model = \"selected\"\n").expect("write selected config");
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.to_path_buf())
|
||||
.loader_overrides(LoaderOverrides {
|
||||
user_config_path: Some(selected_config_path.abs()),
|
||||
..LoaderOverrides::without_managed_config_for_tests()
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("load selected config");
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
}
|
||||
std::fs::write(&override_path, "model = \"override\"\n").expect("write override");
|
||||
|
||||
session.reload_user_config_layer().await;
|
||||
|
||||
let config = session.get_config().await;
|
||||
assert_eq!(
|
||||
config
|
||||
.config_layer_stack
|
||||
.effective_user_config()
|
||||
.expect("merged user config")
|
||||
.get("model")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("selected")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reload_user_config_layer_refreshes_hooks() -> anyhow::Result<()> {
|
||||
let session = make_session_with_config(|config| {
|
||||
|
||||
Reference in New Issue
Block a user