mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
Rewrite branch to config-only
This commit is contained in:
Generated
+9
@@ -1488,6 +1488,7 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-protocol",
|
||||
@@ -1537,6 +1538,7 @@ dependencies = [
|
||||
"clap",
|
||||
"codex-app-server-protocol",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-utils-cli",
|
||||
@@ -1643,6 +1645,7 @@ dependencies = [
|
||||
"clap",
|
||||
"codex-connectors",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-git",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-cli",
|
||||
@@ -1671,6 +1674,7 @@ dependencies = [
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-exec",
|
||||
"codex-exec-server",
|
||||
"codex-execpolicy",
|
||||
"codex-features",
|
||||
"codex-login",
|
||||
@@ -1767,6 +1771,7 @@ dependencies = [
|
||||
"codex-client",
|
||||
"codex-cloud-tasks-client",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-login",
|
||||
"codex-tui",
|
||||
"codex-utils-cli",
|
||||
@@ -1975,6 +1980,7 @@ dependencies = [
|
||||
"codex-arg0",
|
||||
"codex-cloud-requirements",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-feedback",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
@@ -2240,6 +2246,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"codex-arg0",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
@@ -2587,6 +2594,7 @@ dependencies = [
|
||||
"codex-client",
|
||||
"codex-cloud-requirements",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-file-search",
|
||||
@@ -2680,6 +2688,7 @@ dependencies = [
|
||||
"codex-client",
|
||||
"codex-cloud-requirements",
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-file-search",
|
||||
|
||||
@@ -29,6 +29,7 @@ tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
codex-exec-server = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
@@ -870,10 +870,17 @@ mod tests {
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
async fn build_test_config() -> Config {
|
||||
match ConfigBuilder::default().build().await {
|
||||
match ConfigBuilder::default()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
|
||||
.expect("default config should load"),
|
||||
Err(_) => Config::load_default_with_cli_overrides(
|
||||
Vec::new(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.expect("default config should load"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
|
||||
@@ -2097,9 +2097,12 @@ impl TestClientTracing {
|
||||
}
|
||||
.parse_overrides()
|
||||
.map_err(|e| anyhow::anyhow!("error parsing -c overrides: {e}"))?;
|
||||
let config = Config::load_with_cli_overrides(cli_kv_overrides)
|
||||
.await
|
||||
.context("error loading config")?;
|
||||
let config = Config::load_with_cli_overrides(
|
||||
cli_kv_overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.context("error loading config")?;
|
||||
let otel_provider = codex_core::otel_init::build_provider(
|
||||
&config,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
|
||||
@@ -511,7 +511,7 @@ impl CodexMessageProcessor {
|
||||
.cli_overrides(self.cli_overrides.clone())
|
||||
.fallback_cwd(fallback_cwd)
|
||||
.cloud_requirements(cloud_requirements)
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
@@ -5689,7 +5689,7 @@ impl CodexMessageProcessor {
|
||||
let edits = vec![ConfigEdit::SetSkillConfig { path, enabled }];
|
||||
let result = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits(edits)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await;
|
||||
|
||||
match result {
|
||||
@@ -7785,7 +7785,7 @@ async fn sync_default_client_residency_requirement(
|
||||
match codex_core::config::ConfigBuilder::default()
|
||||
.cli_overrides(cli_overrides.to_vec())
|
||||
.cloud_requirements(loader)
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(config) => set_default_client_residency_requirement(config.enforce_residency.value()),
|
||||
@@ -7829,7 +7829,7 @@ async fn derive_config_from_params(
|
||||
.cli_overrides(merged_cli_overrides)
|
||||
.harness_overrides(typesafe_overrides)
|
||||
.cloud_requirements(cloud_requirements.clone())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -7858,7 +7858,7 @@ async fn derive_config_for_cwd(
|
||||
.harness_overrides(typesafe_overrides)
|
||||
.fallback_cwd(cwd)
|
||||
.cloud_requirements(cloud_requirements.clone())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ impl ConfigApi {
|
||||
self.cli_overrides.clone(),
|
||||
self.loader_overrides.clone(),
|
||||
cloud_requirements,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -742,10 +742,17 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
async fn build_test_config() -> Config {
|
||||
match ConfigBuilder::default().build().await {
|
||||
match ConfigBuilder::default()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
|
||||
.expect("default config should load"),
|
||||
Err(_) => Config::load_default_with_cli_overrides(
|
||||
Vec::new(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.expect("default config should load"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -397,7 +397,7 @@ pub async fn run_main_with_transport(
|
||||
let cloud_requirements = match ConfigBuilder::default()
|
||||
.cli_overrides(cli_kv_overrides.clone())
|
||||
.loader_overrides(loader_overrides.clone())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(config) => {
|
||||
@@ -441,19 +441,22 @@ pub async fn run_main_with_transport(
|
||||
.cli_overrides(cli_kv_overrides.clone())
|
||||
.loader_overrides(loader_overrides)
|
||||
.cloud_requirements(cloud_requirements.clone())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
let message = config_warning_from_error("Invalid configuration; using defaults.", &err);
|
||||
config_warnings.push(message);
|
||||
Config::load_default_with_cli_overrides(cli_kv_overrides.clone()).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("error loading default config after config error: {e}"),
|
||||
)
|
||||
})?
|
||||
let file_system = codex_exec_server::Environment::default().get_filesystem();
|
||||
Config::load_default_with_cli_overrides(cli_kv_overrides.clone(), file_system)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("error loading default config after config error: {e}"),
|
||||
)
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-connectors = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -27,6 +27,7 @@ pub async fn run_apply_command(
|
||||
.config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ codex-utils-cli = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
|
||||
@@ -436,7 +436,9 @@ async fn build_debug_sandbox_config(
|
||||
.codex_home(codex_home.clone())
|
||||
.fallback_cwd(Some(codex_home));
|
||||
}
|
||||
builder.build().await
|
||||
builder
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
}
|
||||
|
||||
fn config_uses_permission_profiles(config: &Config) -> bool {
|
||||
|
||||
@@ -372,7 +372,12 @@ async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config
|
||||
}
|
||||
};
|
||||
|
||||
match Config::load_with_cli_overrides(cli_overrides).await {
|
||||
match Config::load_with_cli_overrides(
|
||||
cli_overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading configuration: {e}");
|
||||
|
||||
@@ -884,6 +884,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
let config = Config::load_with_cli_overrides_and_harness_overrides(
|
||||
cli_kv_overrides,
|
||||
overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await?;
|
||||
let mut rows = Vec::with_capacity(FEATURES.len());
|
||||
@@ -923,7 +924,7 @@ async fn enable_feature_in_config(interactive: &TuiCli, feature: &str) -> anyhow
|
||||
ConfigEditsBuilder::new(&codex_home)
|
||||
.with_profile(interactive.config_profile.as_deref())
|
||||
.set_feature_enabled(feature, /*enabled*/ true)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await?;
|
||||
println!("Enabled feature `{feature}` in config.toml.");
|
||||
maybe_print_under_development_feature_warning(&codex_home, interactive, feature);
|
||||
@@ -936,7 +937,7 @@ async fn disable_feature_in_config(interactive: &TuiCli, feature: &str) -> anyho
|
||||
ConfigEditsBuilder::new(&codex_home)
|
||||
.with_profile(interactive.config_profile.as_deref())
|
||||
.set_feature_enabled(feature, /*enabled*/ false)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await?;
|
||||
println!("Disabled feature `{feature}` in config.toml.");
|
||||
Ok(())
|
||||
@@ -976,8 +977,12 @@ async fn run_debug_clear_memories_command(
|
||||
config_profile: interactive.config_profile.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let config =
|
||||
Config::load_with_cli_overrides_and_harness_overrides(cli_kv_overrides, overrides).await?;
|
||||
let config = Config::load_with_cli_overrides_and_harness_overrides(
|
||||
cli_kv_overrides,
|
||||
overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let state_path = state_db_path(config.sqlite_home.as_path());
|
||||
let mut cleared_state_db = false;
|
||||
|
||||
+32
-17
@@ -240,9 +240,12 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
let overrides = config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let config = Config::load_with_cli_overrides(
|
||||
overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
|
||||
let AddArgs {
|
||||
name,
|
||||
@@ -312,7 +315,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
|
||||
ConfigEditsBuilder::new(&codex_home)
|
||||
.replace_mcp_servers(&servers)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?;
|
||||
|
||||
@@ -368,7 +371,7 @@ async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveAr
|
||||
if removed {
|
||||
ConfigEditsBuilder::new(&codex_home)
|
||||
.replace_mcp_servers(&servers)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?;
|
||||
}
|
||||
@@ -386,9 +389,12 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
|
||||
let overrides = config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let config = Config::load_with_cli_overrides(
|
||||
overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
|
||||
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None);
|
||||
|
||||
@@ -437,9 +443,12 @@ async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutAr
|
||||
let overrides = config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let config = Config::load_with_cli_overrides(
|
||||
overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
|
||||
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None);
|
||||
|
||||
@@ -467,9 +476,12 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
|
||||
let overrides = config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let config = Config::load_with_cli_overrides(
|
||||
overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
|
||||
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None);
|
||||
|
||||
@@ -716,9 +728,12 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
|
||||
let overrides = config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let config = Config::load_with_cli_overrides(overrides)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let config = Config::load_with_cli_overrides(
|
||||
overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let mcp_manager = McpManager::new(Arc::new(PluginsManager::new(config.codex_home.clone())));
|
||||
let mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ codex-cloud-tasks-client = { path = "../cloud-tasks-client", features = [
|
||||
] }
|
||||
codex-client = { workspace = true }
|
||||
codex-core = { path = "../core" }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-login = { path = "../login" }
|
||||
codex-tui = { path = "../tui" }
|
||||
codex-utils-cli = { workspace = true }
|
||||
|
||||
@@ -61,7 +61,12 @@ pub fn extract_chatgpt_account_id(token: &str) -> Option<String> {
|
||||
|
||||
pub async fn load_auth_manager() -> Option<AuthManager> {
|
||||
// TODO: pass in cli overrides once cloud tasks properly support them.
|
||||
let config = Config::load_with_cli_overrides(Vec::new()).await.ok()?;
|
||||
let config = Config::load_with_cli_overrides(
|
||||
Vec::new(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
Some(AuthManager::new(
|
||||
config.codex_home,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
|
||||
@@ -71,7 +71,8 @@ async fn apply_role_to_config_inner(
|
||||
role_layer_toml,
|
||||
preserve_current_profile,
|
||||
preserve_current_provider,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ fn preservation_policy(config: &Config, role_layer_toml: &TomlValue) -> (bool, b
|
||||
mod reload {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn build_next_config(
|
||||
pub(super) async fn build_next_config(
|
||||
config: &Config,
|
||||
role_layer_toml: TomlValue,
|
||||
preserve_current_profile: bool,
|
||||
@@ -159,12 +160,15 @@ mod reload {
|
||||
merged_config.profile = None;
|
||||
}
|
||||
|
||||
let file_system = codex_exec_server::Environment::default().get_filesystem();
|
||||
let mut next_config = Config::load_config_with_layer_stack(
|
||||
merged_config,
|
||||
reload_overrides(config, preserve_current_provider),
|
||||
config.codex_home.clone(),
|
||||
config_layer_stack,
|
||||
)?;
|
||||
file_system,
|
||||
)
|
||||
.await?;
|
||||
if preserve_current_profile {
|
||||
next_config.active_profile = config.active_profile.clone();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::AgentsToml;
|
||||
use super::ConfigToml;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use serde::Deserialize;
|
||||
@@ -14,17 +15,18 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
pub(crate) fn load_agent_roles(
|
||||
pub(crate) async fn load_agent_roles(
|
||||
cfg: &ConfigToml,
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<BTreeMap<String, AgentRoleConfig>> {
|
||||
let layers = config_layer_stack.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
);
|
||||
if layers.is_empty() {
|
||||
return load_agent_roles_without_layers(cfg);
|
||||
return load_agent_roles_without_layers(cfg, file_system).await;
|
||||
}
|
||||
|
||||
let mut roles: BTreeMap<String, AgentRoleConfig> = BTreeMap::new();
|
||||
@@ -40,13 +42,14 @@ pub(crate) fn load_agent_roles(
|
||||
};
|
||||
if let Some(agents_toml) = agents_toml {
|
||||
for (declared_role_name, role_toml) in &agents_toml.roles {
|
||||
let (role_name, role) = match read_declared_role(declared_role_name, role_toml) {
|
||||
Ok(role) => role,
|
||||
Err(err) => {
|
||||
push_agent_role_warning(startup_warnings, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let (role_name, role) =
|
||||
match read_declared_role(declared_role_name, role_toml, file_system).await {
|
||||
Ok(role) => role,
|
||||
Err(err) => {
|
||||
push_agent_role_warning(startup_warnings, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(config_file) = role.config_file.clone() {
|
||||
declared_role_files.insert(config_file);
|
||||
}
|
||||
@@ -71,7 +74,10 @@ pub(crate) fn load_agent_roles(
|
||||
config_folder.as_path().join("agents").as_path(),
|
||||
&declared_role_files,
|
||||
startup_warnings,
|
||||
)? {
|
||||
file_system,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if layer_roles.contains_key(&role_name) {
|
||||
push_agent_role_warning(
|
||||
startup_warnings,
|
||||
@@ -113,13 +119,15 @@ fn push_agent_role_warning(startup_warnings: &mut Vec<String>, err: std::io::Err
|
||||
startup_warnings.push(message);
|
||||
}
|
||||
|
||||
fn load_agent_roles_without_layers(
|
||||
async fn load_agent_roles_without_layers(
|
||||
cfg: &ConfigToml,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<BTreeMap<String, AgentRoleConfig>> {
|
||||
let mut roles = BTreeMap::new();
|
||||
if let Some(agents_toml) = cfg.agents.as_ref() {
|
||||
for (declared_role_name, role_toml) in &agents_toml.roles {
|
||||
let (role_name, role) = read_declared_role(declared_role_name, role_toml)?;
|
||||
let (role_name, role) =
|
||||
read_declared_role(declared_role_name, role_toml, file_system).await?;
|
||||
validate_required_agent_role_description(&role_name, role.description.as_deref())?;
|
||||
|
||||
if roles.insert(role_name.clone(), role).is_some() {
|
||||
@@ -134,14 +142,19 @@ fn load_agent_roles_without_layers(
|
||||
Ok(roles)
|
||||
}
|
||||
|
||||
fn read_declared_role(
|
||||
async fn read_declared_role(
|
||||
declared_role_name: &str,
|
||||
role_toml: &AgentRoleToml,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<(String, AgentRoleConfig)> {
|
||||
let mut role = agent_role_config_from_toml(declared_role_name, role_toml)?;
|
||||
validate_agent_role_config_file(declared_role_name, role.config_file.as_deref(), file_system)
|
||||
.await?;
|
||||
let mut role_name = declared_role_name.to_string();
|
||||
if let Some(config_file) = role.config_file.as_deref() {
|
||||
let parsed_file = read_resolved_agent_role_file(config_file, Some(declared_role_name))?;
|
||||
let parsed_file =
|
||||
read_resolved_agent_role_file(config_file, Some(declared_role_name), file_system)
|
||||
.await?;
|
||||
role_name = parsed_file.role_name;
|
||||
role.description = parsed_file.description.or(role.description);
|
||||
role.nickname_candidates = parsed_file.nickname_candidates.or(role.nickname_candidates);
|
||||
@@ -176,7 +189,6 @@ fn agent_role_config_from_toml(
|
||||
role: &AgentRoleToml,
|
||||
) -> std::io::Result<AgentRoleConfig> {
|
||||
let config_file = role.config_file.as_ref().map(AbsolutePathBuf::to_path_buf);
|
||||
validate_agent_role_config_file(role_name, config_file.as_deref())?;
|
||||
let description = normalize_agent_role_description(
|
||||
&format!("agents.{role_name}.description"),
|
||||
role.description.as_deref(),
|
||||
@@ -293,11 +305,12 @@ pub(crate) fn parse_agent_role_file_contents(
|
||||
})
|
||||
}
|
||||
|
||||
fn read_resolved_agent_role_file(
|
||||
async fn read_resolved_agent_role_file(
|
||||
path: &Path,
|
||||
role_name_hint: Option<&str>,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<ResolvedAgentRoleFile> {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
let contents = super::read_text_file(path, file_system).await?;
|
||||
parse_agent_role_file_contents(
|
||||
&contents,
|
||||
path,
|
||||
@@ -359,24 +372,29 @@ fn validate_agent_role_file_developer_instructions(
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_agent_role_config_file(
|
||||
async fn validate_agent_role_config_file(
|
||||
role_name: &str,
|
||||
config_file: Option<&Path>,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<()> {
|
||||
let Some(config_file) = config_file else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let metadata = std::fs::metadata(config_file).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"agents.{role_name}.config_file must point to an existing file at {}: {e}",
|
||||
config_file.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
if metadata.is_file() {
|
||||
let absolute_path = super::absolute_path_for_filesystem_read(config_file)?;
|
||||
let metadata = file_system
|
||||
.get_metadata(&absolute_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"agents.{role_name}.config_file must point to an existing file at {}: {e}",
|
||||
config_file.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
if metadata.is_file {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
@@ -441,25 +459,31 @@ fn normalize_agent_role_nickname_candidates(
|
||||
Ok(Some(normalized_candidates))
|
||||
}
|
||||
|
||||
fn discover_agent_roles_in_dir(
|
||||
async fn discover_agent_roles_in_dir(
|
||||
agents_dir: &Path,
|
||||
declared_role_files: &BTreeSet<PathBuf>,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<BTreeMap<String, AgentRoleConfig>> {
|
||||
let mut roles = BTreeMap::new();
|
||||
|
||||
for agent_file in collect_agent_role_files(agents_dir)? {
|
||||
for agent_file in collect_agent_role_files(agents_dir, file_system).await? {
|
||||
if declared_role_files.contains(&agent_file) {
|
||||
continue;
|
||||
}
|
||||
let parsed_file =
|
||||
match read_resolved_agent_role_file(&agent_file, /*role_name_hint*/ None) {
|
||||
Ok(parsed_file) => parsed_file,
|
||||
Err(err) => {
|
||||
push_agent_role_warning(startup_warnings, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let parsed_file = match read_resolved_agent_role_file(
|
||||
&agent_file,
|
||||
/*role_name_hint*/ None,
|
||||
file_system,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(parsed_file) => parsed_file,
|
||||
Err(err) => {
|
||||
push_agent_role_warning(startup_warnings, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let role_name = parsed_file.role_name;
|
||||
if roles.contains_key(&role_name) {
|
||||
push_agent_role_warning(
|
||||
@@ -487,36 +511,36 @@ fn discover_agent_roles_in_dir(
|
||||
Ok(roles)
|
||||
}
|
||||
|
||||
fn collect_agent_role_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
|
||||
async fn collect_agent_role_files(
|
||||
dir: &Path,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<Vec<PathBuf>> {
|
||||
let mut pending_dirs = vec![super::absolute_path_for_filesystem_read(dir)?];
|
||||
let mut files = Vec::new();
|
||||
collect_agent_role_files_recursive(dir, &mut files)?;
|
||||
files.sort();
|
||||
Ok(files)
|
||||
}
|
||||
while let Some(next_dir) = pending_dirs.pop() {
|
||||
let read_dir = match file_system.read_directory(&next_dir).await {
|
||||
Ok(read_dir) => read_dir,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => continue,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
fn collect_agent_role_files_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
|
||||
let read_dir = match std::fs::read_dir(dir) {
|
||||
Ok(read_dir) => read_dir,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
for entry in read_dir {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_type = entry.file_type()?;
|
||||
if file_type.is_dir() {
|
||||
collect_agent_role_files_recursive(&path, files)?;
|
||||
continue;
|
||||
}
|
||||
if file_type.is_file()
|
||||
&& path
|
||||
.extension()
|
||||
.is_some_and(|extension| extension == "toml")
|
||||
{
|
||||
files.push(path);
|
||||
for entry in read_dir {
|
||||
let path = next_dir.join(&entry.file_name)?;
|
||||
if entry.is_directory {
|
||||
pending_dirs.push(path);
|
||||
continue;
|
||||
}
|
||||
if entry.is_file
|
||||
&& path
|
||||
.as_path()
|
||||
.extension()
|
||||
.is_some_and(|extension| extension == "toml")
|
||||
{
|
||||
files.push(path.into_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
files.sort();
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
@@ -33,9 +33,40 @@ use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
|
||||
pub(super) fn load_from_base_config_with_overrides_for_tests(
|
||||
cfg: ConfigToml,
|
||||
overrides: ConfigOverrides,
|
||||
codex_home: PathBuf,
|
||||
) -> std::io::Result<Config> {
|
||||
// Note this ignores requirements.toml enforcement for tests.
|
||||
let config_layer_stack = crate::config_loader::ConfigLayerStack::default();
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|err| std::io::Error::other(format!("failed to build tokio runtime: {err}")))?;
|
||||
let file_system = codex_exec_server::Environment::default().get_filesystem();
|
||||
let (tx, rx) = std::sync::mpsc::sync_channel(1);
|
||||
runtime.spawn(async move {
|
||||
let result = Config::load_config_with_layer_stack(
|
||||
cfg,
|
||||
overrides,
|
||||
codex_home,
|
||||
config_layer_stack,
|
||||
file_system,
|
||||
)
|
||||
.await;
|
||||
let _ = tx.send(result);
|
||||
});
|
||||
rx.recv().map_err(|err| {
|
||||
std::io::Error::other(format!("failed to receive config from runtime: {err}"))
|
||||
})?
|
||||
}
|
||||
|
||||
fn stdio_mcp(command: &str) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
@@ -2997,8 +3028,8 @@ fn loads_compact_prompt_from_file() -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::Result<()> {
|
||||
#[tokio::test]
|
||||
async fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config_layer_stack = ConfigLayerStack::new(
|
||||
Vec::new(),
|
||||
@@ -3012,6 +3043,7 @@ fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::R
|
||||
)
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
let file_system = codex_exec_server::Environment::default().get_filesystem();
|
||||
let config = Config::load_config_with_layer_stack(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides {
|
||||
@@ -3020,7 +3052,9 @@ fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::R
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
config_layer_stack,
|
||||
)?;
|
||||
file_system,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.guardian_developer_instructions.as_deref(),
|
||||
@@ -3030,8 +3064,9 @@ fn load_config_uses_requirements_guardian_developer_instructions() -> std::io::R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_ignores_empty_requirements_guardian_developer_instructions() -> std::io::Result<()> {
|
||||
#[tokio::test]
|
||||
async fn load_config_ignores_empty_requirements_guardian_developer_instructions()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config_layer_stack = ConfigLayerStack::new(
|
||||
Vec::new(),
|
||||
@@ -3043,6 +3078,7 @@ fn load_config_ignores_empty_requirements_guardian_developer_instructions() -> s
|
||||
)
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
let file_system = codex_exec_server::Environment::default().get_filesystem();
|
||||
let config = Config::load_config_with_layer_stack(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides {
|
||||
@@ -3051,7 +3087,9 @@ fn load_config_ignores_empty_requirements_guardian_developer_instructions() -> s
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
config_layer_stack,
|
||||
)?;
|
||||
file_system,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.guardian_developer_instructions, None);
|
||||
|
||||
@@ -4773,8 +4811,9 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> anyhow::Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> anyhow::Result<()>
|
||||
{
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let requirements_toml = crate::config_loader::ConfigRequirementsToml {
|
||||
@@ -4817,6 +4856,7 @@ fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> any
|
||||
crate::config_loader::ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
|
||||
.expect("config layer stack");
|
||||
|
||||
let file_system = codex_exec_server::Environment::default().get_filesystem();
|
||||
let config = Config::load_config_with_layer_stack(
|
||||
fixture.cfg.clone(),
|
||||
ConfigOverrides {
|
||||
@@ -4825,7 +4865,9 @@ fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> any
|
||||
},
|
||||
fixture.codex_home(),
|
||||
config_layer_stack,
|
||||
)?;
|
||||
file_system,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
!config
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::config::types::McpServerConfig;
|
||||
use crate::config::types::Notice;
|
||||
use crate::path_utils::resolve_symlink_write_paths;
|
||||
use crate::path_utils::write_atomically;
|
||||
use anyhow::Context;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_features::FEATURES;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
@@ -11,9 +11,10 @@ use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tokio::task;
|
||||
use std::sync::Arc;
|
||||
use toml_edit::ArrayOfTables;
|
||||
use toml_edit::DocumentMut;
|
||||
use toml_edit::Item as TomlItem;
|
||||
@@ -699,11 +700,11 @@ fn normalize_skill_config_path(path: &Path) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Persist edits using a blocking strategy.
|
||||
pub fn apply_blocking(
|
||||
async fn apply_with_file_system(
|
||||
codex_home: &Path,
|
||||
profile: Option<&str>,
|
||||
edits: &[ConfigEdit],
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> anyhow::Result<()> {
|
||||
if edits.is_empty() {
|
||||
return Ok(());
|
||||
@@ -712,9 +713,9 @@ pub fn apply_blocking(
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let write_paths = resolve_symlink_write_paths(&config_path)?;
|
||||
let serialized = match write_paths.read_path {
|
||||
Some(path) => match std::fs::read_to_string(&path) {
|
||||
Some(path) => match super::read_text_file(path.as_path(), file_system).await {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => String::new(),
|
||||
Err(err) => return Err(err.into()),
|
||||
},
|
||||
None => String::new(),
|
||||
@@ -743,27 +744,33 @@ pub fn apply_blocking(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
write_atomically(&write_paths.write_path, &document.doc.to_string()).with_context(|| {
|
||||
format!(
|
||||
"failed to persist config.toml at {}",
|
||||
write_paths.write_path.display()
|
||||
)
|
||||
})?;
|
||||
let write_path = super::absolute_path_for_filesystem_read(write_paths.write_path.as_path())?;
|
||||
file_system
|
||||
.write_file(&write_path, document.doc.to_string().into_bytes())
|
||||
.await
|
||||
.with_context(|| format!("failed to persist config.toml at {}", write_path.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Persist edits asynchronously by offloading the blocking writer.
|
||||
/// Persist edits asynchronously.
|
||||
pub async fn apply(
|
||||
codex_home: &Path,
|
||||
profile: Option<&str>,
|
||||
edits: Vec<ConfigEdit>,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> anyhow::Result<()> {
|
||||
let codex_home = codex_home.to_path_buf();
|
||||
let profile = profile.map(ToOwned::to_owned);
|
||||
task::spawn_blocking(move || apply_blocking(&codex_home, profile.as_deref(), &edits))
|
||||
.await
|
||||
.context("config persistence task panicked")?
|
||||
apply_with_file_system(codex_home, profile, &edits, &*file_system).await
|
||||
}
|
||||
|
||||
/// Persist edits using a blocking strategy.
|
||||
#[cfg(test)]
|
||||
pub fn apply_blocking(
|
||||
codex_home: &Path,
|
||||
profile: Option<&str>,
|
||||
edits: &[ConfigEdit],
|
||||
) -> anyhow::Result<()> {
|
||||
tests::apply_blocking_for_tests(codex_home, profile, edits)
|
||||
}
|
||||
|
||||
/// Fluent builder to batch config edits and apply them atomically.
|
||||
@@ -976,17 +983,20 @@ impl ConfigEditsBuilder {
|
||||
}
|
||||
|
||||
/// Apply edits on a blocking thread.
|
||||
#[cfg(test)]
|
||||
pub fn apply_blocking(self) -> anyhow::Result<()> {
|
||||
apply_blocking(&self.codex_home, self.profile.as_deref(), &self.edits)
|
||||
}
|
||||
|
||||
/// Apply edits asynchronously via a blocking offload.
|
||||
pub async fn apply(self) -> anyhow::Result<()> {
|
||||
task::spawn_blocking(move || {
|
||||
apply_blocking(&self.codex_home, self.profile.as_deref(), &self.edits)
|
||||
})
|
||||
/// Apply edits asynchronously.
|
||||
pub async fn apply(self, file_system: Arc<dyn ExecutorFileSystem>) -> anyhow::Result<()> {
|
||||
apply(
|
||||
&self.codex_home,
|
||||
self.profile.as_deref(),
|
||||
self.edits,
|
||||
file_system,
|
||||
)
|
||||
.await
|
||||
.context("config persistence task panicked")?
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,55 @@ use std::os::unix::fs::symlink;
|
||||
use tempfile::tempdir;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
pub(super) fn apply_blocking_for_tests(
|
||||
codex_home: &Path,
|
||||
profile: Option<&str>,
|
||||
edits: &[ConfigEdit],
|
||||
) -> anyhow::Result<()> {
|
||||
if edits.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let write_paths = crate::path_utils::resolve_symlink_write_paths(&config_path)?;
|
||||
let serialized = match write_paths.read_path {
|
||||
Some(path) => match std::fs::read_to_string(&path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
|
||||
Err(err) => return Err(err.into()),
|
||||
},
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
let doc = if serialized.is_empty() {
|
||||
DocumentMut::new()
|
||||
} else {
|
||||
serialized.parse::<DocumentMut>()?
|
||||
};
|
||||
|
||||
let profile = profile.map(ToOwned::to_owned).or_else(|| {
|
||||
doc.get("profile")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(ToOwned::to_owned)
|
||||
});
|
||||
|
||||
let mut document = ConfigDocument::new(doc, profile);
|
||||
let mut mutated = false;
|
||||
|
||||
for edit in edits {
|
||||
mutated |= document.apply(edit)?;
|
||||
}
|
||||
|
||||
if !mutated {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
crate::path_utils::write_atomically(&write_paths.write_path, &document.doc.to_string())
|
||||
.map_err(anyhow::Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_set_model_top_level() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
|
||||
+126
-44
@@ -61,6 +61,8 @@ use crate::windows_sandbox::resolve_windows_sandbox_mode;
|
||||
use crate::windows_sandbox::resolve_windows_sandbox_private_desktop;
|
||||
use codex_app_server_protocol::Tools;
|
||||
use codex_app_server_protocol::UserSavedConfig;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_features::Feature;
|
||||
use codex_features::FeatureConfigSource;
|
||||
use codex_features::FeatureOverrides;
|
||||
@@ -96,6 +98,7 @@ use std::collections::HashMap;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::permissions::compile_permission_profile;
|
||||
use crate::config::permissions::get_readable_roots_required_for_codex_runtime;
|
||||
@@ -642,7 +645,7 @@ impl ConfigBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn build(self) -> std::io::Result<Config> {
|
||||
pub async fn build(self, file_system: Arc<dyn ExecutorFileSystem>) -> std::io::Result<Config> {
|
||||
let Self {
|
||||
codex_home,
|
||||
cli_overrides,
|
||||
@@ -652,7 +655,9 @@ impl ConfigBuilder {
|
||||
fallback_cwd,
|
||||
} = self;
|
||||
let codex_home = codex_home.map_or_else(find_codex_home, std::io::Result::Ok)?;
|
||||
if let Err(err) = maybe_migrate_smart_approvals_alias(&codex_home).await {
|
||||
if let Err(err) =
|
||||
maybe_migrate_smart_approvals_alias(&codex_home, file_system.clone()).await
|
||||
{
|
||||
tracing::warn!(error = %err, "failed to migrate smart_approvals feature alias");
|
||||
}
|
||||
let cli_overrides = cli_overrides.unwrap_or_default();
|
||||
@@ -698,7 +703,9 @@ impl ConfigBuilder {
|
||||
harness_overrides,
|
||||
codex_home,
|
||||
config_layer_stack,
|
||||
file_system,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,13 +765,16 @@ fn push_smart_approvals_alias_migration_edits(
|
||||
/// migrated feature value is `true`.
|
||||
/// In all cases it removes the deprecated `smart_approvals` entry so future
|
||||
/// loads only see the canonical feature flag name.
|
||||
async fn maybe_migrate_smart_approvals_alias(codex_home: &Path) -> std::io::Result<bool> {
|
||||
async fn maybe_migrate_smart_approvals_alias(
|
||||
codex_home: &Path,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> std::io::Result<bool> {
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
if !tokio::fs::try_exists(&config_path).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let config_contents = tokio::fs::read_to_string(&config_path).await?;
|
||||
let config_contents = match read_text_file(config_path.as_path(), &*file_system).await {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(false),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let Ok(config_toml) = toml::from_str::<ConfigToml>(&config_contents) else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -799,7 +809,7 @@ async fn maybe_migrate_smart_approvals_alias(codex_home: &Path) -> std::io::Resu
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_edits(edits)
|
||||
.apply()
|
||||
.apply(file_system)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
std::io::Error::other(format!("failed to migrate guardian_approval alias: {err}"))
|
||||
@@ -811,16 +821,18 @@ impl Config {
|
||||
/// This is the preferred way to create an instance of [Config].
|
||||
pub async fn load_with_cli_overrides(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> std::io::Result<Self> {
|
||||
ConfigBuilder::default()
|
||||
.cli_overrides(cli_overrides)
|
||||
.build()
|
||||
.build(file_system)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Load a default configuration when user config files are invalid.
|
||||
pub fn load_default_with_cli_overrides(
|
||||
pub async fn load_default_with_cli_overrides(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> std::io::Result<Self> {
|
||||
let codex_home = find_codex_home()?;
|
||||
let mut merged = toml::Value::try_from(ConfigToml::default()).map_err(|e| {
|
||||
@@ -837,7 +849,9 @@ impl Config {
|
||||
ConfigOverrides::default(),
|
||||
codex_home,
|
||||
ConfigLayerStack::default(),
|
||||
file_system,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// This is a secondary way of creating [Config], which is appropriate when
|
||||
@@ -851,11 +865,12 @@ impl Config {
|
||||
pub async fn load_with_cli_overrides_and_harness_overrides(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
harness_overrides: ConfigOverrides,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> std::io::Result<Self> {
|
||||
ConfigBuilder::default()
|
||||
.cli_overrides(cli_overrides)
|
||||
.harness_overrides(harness_overrides)
|
||||
.build()
|
||||
.build(file_system)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -867,8 +882,9 @@ pub async fn load_config_as_toml_with_cli_overrides(
|
||||
codex_home: &Path,
|
||||
cwd: &AbsolutePathBuf,
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> std::io::Result<ConfigToml> {
|
||||
if let Err(err) = maybe_migrate_smart_approvals_alias(codex_home).await {
|
||||
if let Err(err) = maybe_migrate_smart_approvals_alias(codex_home, file_system.clone()).await {
|
||||
tracing::warn!(error = %err, "failed to migrate smart_approvals feature alias");
|
||||
}
|
||||
let config_layer_stack = load_config_layers_state(
|
||||
@@ -901,8 +917,45 @@ pub(crate) fn deserialize_config_toml_with_base(
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
|
||||
fn load_catalog_json(path: &AbsolutePathBuf) -> std::io::Result<ModelsResponse> {
|
||||
let file_contents = std::fs::read_to_string(path)?;
|
||||
fn absolute_path_for_filesystem_read(path: &Path) -> std::io::Result<AbsolutePathBuf> {
|
||||
let absolute_path = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
std::env::current_dir()?.join(path)
|
||||
};
|
||||
AbsolutePathBuf::try_from(absolute_path.as_path())
|
||||
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))
|
||||
}
|
||||
|
||||
async fn read_text_file_with_executor_filesystem(
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
path: &Path,
|
||||
) -> std::io::Result<String> {
|
||||
let absolute_path = absolute_path_for_filesystem_read(path)?;
|
||||
let bytes = file_system.read_file(&absolute_path).await?;
|
||||
String::from_utf8(bytes).map_err(|err| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!(
|
||||
"failed to decode UTF-8 from {}: {err}",
|
||||
absolute_path.display()
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
path: &Path,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<String> {
|
||||
read_text_file_with_executor_filesystem(file_system, path).await
|
||||
}
|
||||
|
||||
async fn load_catalog_json(
|
||||
path: &AbsolutePathBuf,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<ModelsResponse> {
|
||||
let file_contents = read_text_file(path.as_path(), file_system).await?;
|
||||
let catalog = serde_json::from_str::<ModelsResponse>(&file_contents).map_err(|err| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
@@ -924,12 +977,14 @@ fn load_catalog_json(path: &AbsolutePathBuf) -> std::io::Result<ModelsResponse>
|
||||
Ok(catalog)
|
||||
}
|
||||
|
||||
fn load_model_catalog(
|
||||
async fn load_model_catalog(
|
||||
model_catalog_json: Option<AbsolutePathBuf>,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<Option<ModelsResponse>> {
|
||||
model_catalog_json
|
||||
.map(|path| load_catalog_json(&path))
|
||||
.transpose()
|
||||
let Some(path) = model_catalog_json else {
|
||||
return Ok(None);
|
||||
};
|
||||
load_catalog_json(&path, file_system).await.map(Some)
|
||||
}
|
||||
|
||||
fn filter_mcp_servers_by_requirements(
|
||||
@@ -1154,20 +1209,26 @@ pub(crate) fn set_project_trust_level_inner(
|
||||
|
||||
/// Patch `CODEX_HOME/config.toml` project state to set trust level.
|
||||
/// Use with caution.
|
||||
pub fn set_project_trust_level(
|
||||
pub async fn set_project_trust_level(
|
||||
codex_home: &Path,
|
||||
project_path: &Path,
|
||||
trust_level: TrustLevel,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> anyhow::Result<()> {
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.set_project_trust_level(project_path, trust_level)
|
||||
.apply_blocking()
|
||||
.apply(file_system)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Save the default OSS provider preference to config.toml
|
||||
pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::Result<()> {
|
||||
pub async fn set_default_oss_provider(
|
||||
codex_home: &Path,
|
||||
provider: &str,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> std::io::Result<()> {
|
||||
// Validate that the provider is one of the known OSS providers
|
||||
match provider {
|
||||
LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => {
|
||||
@@ -1197,7 +1258,8 @@ pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::R
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_edits(edits)
|
||||
.apply_blocking()
|
||||
.apply(file_system)
|
||||
.await
|
||||
.map_err(|err| std::io::Error::other(format!("failed to persist config.toml: {err}")))
|
||||
}
|
||||
|
||||
@@ -2089,16 +2151,15 @@ impl Config {
|
||||
overrides: ConfigOverrides,
|
||||
codex_home: PathBuf,
|
||||
) -> std::io::Result<Self> {
|
||||
// Note this ignores requirements.toml enforcement for tests.
|
||||
let config_layer_stack = ConfigLayerStack::default();
|
||||
Self::load_config_with_layer_stack(cfg, overrides, codex_home, config_layer_stack)
|
||||
tests::load_from_base_config_with_overrides_for_tests(cfg, overrides, codex_home)
|
||||
}
|
||||
|
||||
pub(crate) fn load_config_with_layer_stack(
|
||||
pub(crate) async fn load_config_with_layer_stack(
|
||||
cfg: ConfigToml,
|
||||
overrides: ConfigOverrides,
|
||||
codex_home: PathBuf,
|
||||
config_layer_stack: ConfigLayerStack,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> std::io::Result<Self> {
|
||||
validate_reserved_model_provider_ids(&cfg.model_providers)
|
||||
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
|
||||
@@ -2115,7 +2176,7 @@ impl Config {
|
||||
network: network_requirements,
|
||||
} = config_layer_stack.requirements().clone();
|
||||
|
||||
let user_instructions = Self::load_instructions(Some(&codex_home));
|
||||
let user_instructions = Self::load_instructions(Some(&codex_home), &*file_system).await;
|
||||
let mut startup_warnings = Vec::new();
|
||||
|
||||
// Destructure ConfigOverrides fully to ensure all overrides are applied.
|
||||
@@ -2243,8 +2304,10 @@ impl Config {
|
||||
None => WindowsSandboxLevel::from_features(&features),
|
||||
};
|
||||
let memories_root = memory_root(&codex_home);
|
||||
std::fs::create_dir_all(&memories_root)?;
|
||||
let memories_root = AbsolutePathBuf::from_absolute_path(&memories_root)?;
|
||||
file_system
|
||||
.create_directory(&memories_root, CreateDirectoryOptions { recursive: true })
|
||||
.await?;
|
||||
if !additional_writable_roots
|
||||
.iter()
|
||||
.any(|existing| existing == &memories_root)
|
||||
@@ -2356,8 +2419,13 @@ impl Config {
|
||||
.unwrap_or(WebSearchMode::Cached);
|
||||
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
|
||||
|
||||
let agent_roles =
|
||||
agent_roles::load_agent_roles(&cfg, &config_layer_stack, &mut startup_warnings)?;
|
||||
let agent_roles = agent_roles::load_agent_roles(
|
||||
&cfg,
|
||||
&config_layer_stack,
|
||||
&mut startup_warnings,
|
||||
&*file_system,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let openai_base_url = cfg
|
||||
.openai_base_url
|
||||
@@ -2522,8 +2590,12 @@ impl Config {
|
||||
.model_instructions_file
|
||||
.as_ref()
|
||||
.or(cfg.model_instructions_file.as_ref());
|
||||
let file_base_instructions =
|
||||
Self::try_read_non_empty_file(model_instructions_path, "model instructions file")?;
|
||||
let file_base_instructions = Self::try_read_non_empty_file(
|
||||
model_instructions_path,
|
||||
"model instructions file",
|
||||
&*file_system,
|
||||
)
|
||||
.await?;
|
||||
let base_instructions = base_instructions.or(file_base_instructions);
|
||||
let developer_instructions = developer_instructions.or(cfg.developer_instructions);
|
||||
let guardian_developer_instructions = guardian_developer_instructions_from_requirements(
|
||||
@@ -2545,7 +2617,9 @@ impl Config {
|
||||
let file_compact_prompt = Self::try_read_non_empty_file(
|
||||
experimental_compact_prompt_path,
|
||||
"experimental compact prompt file",
|
||||
)?;
|
||||
&*file_system,
|
||||
)
|
||||
.await?;
|
||||
let compact_prompt = compact_prompt.or(file_compact_prompt);
|
||||
let js_repl_node_path = js_repl_node_path_override
|
||||
.or(config_profile.js_repl_node_path.map(Into::into))
|
||||
@@ -2573,7 +2647,9 @@ impl Config {
|
||||
.model_catalog_json
|
||||
.clone()
|
||||
.or(cfg.model_catalog_json.clone()),
|
||||
)?;
|
||||
&*file_system,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let log_dir = cfg
|
||||
.log_dir
|
||||
@@ -2851,12 +2927,15 @@ impl Config {
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn load_instructions(codex_dir: Option<&Path>) -> Option<String> {
|
||||
async fn load_instructions(
|
||||
codex_dir: Option<&Path>,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> Option<String> {
|
||||
let base = codex_dir?;
|
||||
for candidate in [LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME] {
|
||||
let mut path = base.to_path_buf();
|
||||
path.push(candidate);
|
||||
if let Ok(contents) = std::fs::read_to_string(&path) {
|
||||
if let Ok(contents) = read_text_file(path.as_path(), file_system).await {
|
||||
let trimmed = contents.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Some(trimmed.to_string());
|
||||
@@ -2869,20 +2948,23 @@ impl Config {
|
||||
/// If `path` is `Some`, attempts to read the file at the given path and
|
||||
/// returns its contents as a trimmed `String`. If the file is empty, or
|
||||
/// is `Some` but cannot be read, returns an `Err`.
|
||||
fn try_read_non_empty_file(
|
||||
async fn try_read_non_empty_file(
|
||||
path: Option<&AbsolutePathBuf>,
|
||||
context: &str,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> std::io::Result<Option<String>> {
|
||||
let Some(path) = path else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let contents = std::fs::read_to_string(path).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
e.kind(),
|
||||
format!("failed to read {context} {}: {e}", path.display()),
|
||||
)
|
||||
})?;
|
||||
let contents = read_text_file(path.as_path(), file_system)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
e.kind(),
|
||||
format!("failed to read {context} {}: {e}", path.display()),
|
||||
)
|
||||
})?;
|
||||
|
||||
let s = contents.trim().to_string();
|
||||
if s.is_empty() {
|
||||
|
||||
@@ -15,7 +15,6 @@ use crate::config_loader::merge_toml_values;
|
||||
use crate::path_utils;
|
||||
use crate::path_utils::SymlinkWritePaths;
|
||||
use crate::path_utils::resolve_symlink_write_paths;
|
||||
use crate::path_utils::write_atomically;
|
||||
use codex_app_server_protocol::Config as ApiConfig;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
use codex_app_server_protocol::ConfigLayerMetadata;
|
||||
@@ -29,13 +28,14 @@ use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_app_server_protocol::OverriddenMetadata;
|
||||
use codex_app_server_protocol::WriteStatus;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tokio::task;
|
||||
use toml::Value as TomlValue;
|
||||
use toml_edit::Item as TomlItem;
|
||||
|
||||
@@ -114,6 +114,7 @@ pub struct ConfigService {
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
}
|
||||
|
||||
impl ConfigService {
|
||||
@@ -122,21 +123,27 @@ impl ConfigService {
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
file_system,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_defaults(codex_home: PathBuf) -> Self {
|
||||
pub fn new_with_defaults(
|
||||
codex_home: PathBuf,
|
||||
file_system: Arc<dyn ExecutorFileSystem>,
|
||||
) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
cli_overrides: Vec::new(),
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
file_system,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +162,7 @@ impl ConfigService {
|
||||
.loader_overrides(self.loader_overrides.clone())
|
||||
.fallback_cwd(Some(cwd.to_path_buf()))
|
||||
.cloud_requirements(self.cloud_requirements.clone())
|
||||
.build()
|
||||
.build(self.file_system.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
ConfigServiceError::io("failed to read configuration layers", err)
|
||||
@@ -276,7 +283,7 @@ impl ConfigService {
|
||||
.map_err(|err| ConfigServiceError::io("failed to load configuration", err))?;
|
||||
let user_layer = match layers.get_user_layer() {
|
||||
Some(layer) => Cow::Borrowed(layer),
|
||||
None => Cow::Owned(create_empty_user_layer(&allowed_path).await?),
|
||||
None => Cow::Owned(create_empty_user_layer(&allowed_path, &*self.file_system).await?),
|
||||
};
|
||||
|
||||
if let Some(expected) = expected_version.as_deref()
|
||||
@@ -381,7 +388,7 @@ impl ConfigService {
|
||||
if !config_edits.is_empty() {
|
||||
ConfigEditsBuilder::new(&self.codex_home)
|
||||
.with_edits(config_edits)
|
||||
.apply()
|
||||
.apply(self.file_system.clone())
|
||||
.await
|
||||
.map_err(|err| ConfigServiceError::anyhow("failed to persist config.toml", err))?;
|
||||
}
|
||||
@@ -427,6 +434,7 @@ impl ConfigService {
|
||||
|
||||
async fn create_empty_user_layer(
|
||||
config_toml: &AbsolutePathBuf,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> Result<ConfigLayerEntry, ConfigServiceError> {
|
||||
let SymlinkWritePaths {
|
||||
read_path,
|
||||
@@ -434,12 +442,12 @@ async fn create_empty_user_layer(
|
||||
} = resolve_symlink_write_paths(config_toml.as_path())
|
||||
.map_err(|err| ConfigServiceError::io("failed to resolve user config path", err))?;
|
||||
let toml_value = match read_path {
|
||||
Some(path) => match tokio::fs::read_to_string(&path).await {
|
||||
Some(path) => match super::read_text_file(path.as_path(), file_system).await {
|
||||
Ok(contents) => toml::from_str(&contents).map_err(|e| {
|
||||
ConfigServiceError::toml("failed to parse existing user config.toml", e)
|
||||
})?,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
write_empty_user_config(write_path.clone()).await?;
|
||||
write_empty_user_config(write_path.clone(), file_system).await?;
|
||||
TomlValue::Table(toml::map::Map::new())
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -450,7 +458,7 @@ async fn create_empty_user_layer(
|
||||
}
|
||||
},
|
||||
None => {
|
||||
write_empty_user_config(write_path).await?;
|
||||
write_empty_user_config(write_path, file_system).await?;
|
||||
TomlValue::Table(toml::map::Map::new())
|
||||
}
|
||||
};
|
||||
@@ -462,10 +470,15 @@ async fn create_empty_user_layer(
|
||||
))
|
||||
}
|
||||
|
||||
async fn write_empty_user_config(write_path: PathBuf) -> Result<(), ConfigServiceError> {
|
||||
task::spawn_blocking(move || write_atomically(&write_path, ""))
|
||||
async fn write_empty_user_config(
|
||||
write_path: PathBuf,
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
) -> Result<(), ConfigServiceError> {
|
||||
let absolute_path = super::absolute_path_for_filesystem_read(write_path.as_path())
|
||||
.map_err(|err| ConfigServiceError::io("failed to resolve user config path", err))?;
|
||||
file_system
|
||||
.write_file(&absolute_path, Vec::new())
|
||||
.await
|
||||
.map_err(|err| ConfigServiceError::anyhow("config persistence task panicked", err.into()))?
|
||||
.map_err(|err| ConfigServiceError::io("failed to create empty user config.toml", err))
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,10 @@ unified_exec = true
|
||||
"#;
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), original)?;
|
||||
|
||||
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
|
||||
let service = ConfigService::new_with_defaults(
|
||||
tmp.path().to_path_buf(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
);
|
||||
service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
|
||||
@@ -108,7 +111,10 @@ async fn write_value_supports_nested_app_paths() -> Result<()> {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?;
|
||||
|
||||
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
|
||||
let service = ConfigService::new_with_defaults(
|
||||
tmp.path().to_path_buf(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
);
|
||||
service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
|
||||
@@ -304,7 +310,10 @@ async fn version_conflict_rejected() {
|
||||
let user_path = tmp.path().join(CONFIG_TOML_FILE);
|
||||
std::fs::write(&user_path, "model = \"user\"").unwrap();
|
||||
|
||||
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
|
||||
let service = ConfigService::new_with_defaults(
|
||||
tmp.path().to_path_buf(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
);
|
||||
let error = service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
|
||||
@@ -327,7 +336,10 @@ async fn write_value_defaults_to_user_config_path() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap();
|
||||
|
||||
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
|
||||
let service = ConfigService::new_with_defaults(
|
||||
tmp.path().to_path_buf(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
);
|
||||
service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: None,
|
||||
@@ -391,7 +403,10 @@ async fn reserved_builtin_provider_override_rejected() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"\n").unwrap();
|
||||
|
||||
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
|
||||
let service = ConfigService::new_with_defaults(
|
||||
tmp.path().to_path_buf(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
);
|
||||
let error = service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
|
||||
@@ -650,7 +665,10 @@ alpha = "a"
|
||||
|
||||
std::fs::write(&path, base)?;
|
||||
|
||||
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
|
||||
let service = ConfigService::new_with_defaults(
|
||||
tmp.path().to_path_buf(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
);
|
||||
service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(path.display().to_string()),
|
||||
|
||||
@@ -1002,8 +1002,8 @@ fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_
|
||||
assert_eq!(guardian_config.model, Some("active-model".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_review_session_config_uses_requirements_guardian_override() {
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_uses_requirements_guardian_override() {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let workspace = tempfile::tempdir().expect("create temp dir");
|
||||
let config_layer_stack = ConfigLayerStack::new(
|
||||
@@ -1017,6 +1017,7 @@ fn guardian_review_session_config_uses_requirements_guardian_override() {
|
||||
},
|
||||
)
|
||||
.expect("config layer stack");
|
||||
let file_system = codex_exec_server::Environment::default().get_filesystem();
|
||||
let parent_config = Config::load_config_with_layer_stack(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides {
|
||||
@@ -1025,7 +1026,9 @@ fn guardian_review_session_config_uses_requirements_guardian_override() {
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
config_layer_stack,
|
||||
file_system,
|
||||
)
|
||||
.await
|
||||
.expect("load config");
|
||||
|
||||
let guardian_config =
|
||||
@@ -1038,13 +1041,15 @@ fn guardian_review_session_config_uses_requirements_guardian_override() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override() {
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override()
|
||||
{
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let workspace = tempfile::tempdir().expect("create temp dir");
|
||||
let config_layer_stack =
|
||||
ConfigLayerStack::new(Vec::new(), Default::default(), Default::default())
|
||||
.expect("config layer stack");
|
||||
let file_system = codex_exec_server::Environment::default().get_filesystem();
|
||||
let parent_config = Config::load_config_with_layer_stack(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides {
|
||||
@@ -1053,7 +1058,9 @@ fn guardian_review_session_config_uses_default_guardian_policy_without_requireme
|
||||
},
|
||||
codex_home.path().to_path_buf(),
|
||||
config_layer_stack,
|
||||
file_system,
|
||||
)
|
||||
.await
|
||||
.expect("load config");
|
||||
|
||||
let guardian_config =
|
||||
|
||||
@@ -213,7 +213,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
|
||||
if let Err(err) = ConfigEditsBuilder::new(&codex_home)
|
||||
.replace_mcp_servers(&servers)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
warn!("failed to persist MCP dependencies for mentioned skills: {err}");
|
||||
|
||||
@@ -1295,7 +1295,7 @@ async fn persist_codex_app_tool_approval(
|
||||
],
|
||||
value: value("approve"),
|
||||
}])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ pub async fn maybe_migrate_personality(
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.set_personality(Some(Personality::Pragmatic))
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
io::Error::other(format!("failed to persist personality migration: {err}"))
|
||||
|
||||
@@ -689,19 +689,22 @@ impl PluginsManager {
|
||||
.await
|
||||
.map_err(PluginInstallError::join)??;
|
||||
|
||||
ConfigService::new_with_defaults(self.codex_home.clone())
|
||||
.write_value(ConfigValueWriteParams {
|
||||
key_path: format!("plugins.{}", result.plugin_id.as_key()),
|
||||
value: json!({
|
||||
"enabled": true,
|
||||
}),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
})
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(PluginInstallError::from)?;
|
||||
ConfigService::new_with_defaults(
|
||||
self.codex_home.clone(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.write_value(ConfigValueWriteParams {
|
||||
key_path: format!("plugins.{}", result.plugin_id.as_key()),
|
||||
value: json!({
|
||||
"enabled": true,
|
||||
}),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
})
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(PluginInstallError::from)?;
|
||||
|
||||
let analytics_events_client = match self.analytics_events_client.read() {
|
||||
Ok(client) => client.clone(),
|
||||
@@ -759,7 +762,7 @@ impl PluginsManager {
|
||||
.with_edits([ConfigEdit::ClearPath {
|
||||
segments: vec!["plugins".to_string(), plugin_id.as_key()],
|
||||
}])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await?;
|
||||
|
||||
let analytics_events_client = match self.analytics_events_client.read() {
|
||||
@@ -958,7 +961,7 @@ impl PluginsManager {
|
||||
} else {
|
||||
ConfigEditsBuilder::new(&self.codex_home)
|
||||
.with_edits(config_edits)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
};
|
||||
self.clear_cache();
|
||||
|
||||
@@ -350,7 +350,7 @@ async fn run_windows_sandbox_setup_and_persist(
|
||||
.with_profile(active_profile.as_deref())
|
||||
.set_windows_sandbox_mode(windows_sandbox_setup_mode_tag(mode))
|
||||
.clear_legacy_windows_sandbox_keys()
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("failed to persist windows sandbox mode: {err}"))
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ pub async fn load_default_config_for_test(codex_home: &TempDir) -> Config {
|
||||
ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(default_test_overrides())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.expect("defaults for test should always succeed")
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ codex-app-server-client = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
|
||||
@@ -266,6 +266,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
&codex_home,
|
||||
&config_cwd,
|
||||
cli_kv_overrides.clone(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -365,7 +366,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
.cli_overrides(cli_kv_overrides)
|
||||
.harness_overrides(overrides)
|
||||
.cloud_requirements(cloud_requirements)
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await?;
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
@@ -1632,7 +1633,8 @@ mod tests {
|
||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||
|
||||
fn test_tracing_subscriber() -> impl tracing::Subscriber + Send + Sync {
|
||||
let provider = SdkTracerProvider::builder().build();
|
||||
let provider = SdkTracerProvider::builder()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem());
|
||||
let tracer = provider.tracer("codex-exec-tests");
|
||||
tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer))
|
||||
}
|
||||
@@ -1853,7 +1855,7 @@ mod tests {
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.expect("build default config");
|
||||
|
||||
@@ -1877,7 +1879,7 @@ mod tests {
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(cwd.path().to_path_buf()))
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.expect("build auto-review config");
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ workspace = true
|
||||
anyhow = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
|
||||
@@ -190,8 +190,12 @@ impl CodexToolCallParam {
|
||||
.map(|(k, v)| (k, json_to_toml(v)))
|
||||
.collect();
|
||||
|
||||
let cfg =
|
||||
Config::load_with_cli_overrides_and_harness_overrides(cli_overrides, overrides).await?;
|
||||
let cfg = Config::load_with_cli_overrides_and_harness_overrides(
|
||||
cli_overrides,
|
||||
overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((prompt, cfg))
|
||||
}
|
||||
|
||||
@@ -63,11 +63,14 @@ pub async fn run_main(
|
||||
format!("error parsing -c overrides: {e}"),
|
||||
)
|
||||
})?;
|
||||
let config = Config::load_with_cli_overrides(cli_kv_overrides)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
|
||||
})?;
|
||||
let config = Config::load_with_cli_overrides(
|
||||
cli_kv_overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
|
||||
})?;
|
||||
|
||||
let otel = codex_core::otel_init::build_provider(
|
||||
&config,
|
||||
|
||||
@@ -37,6 +37,7 @@ codex-chatgpt = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
|
||||
+30
-21
@@ -698,7 +698,7 @@ async fn prepare_startup_tooltip_override(
|
||||
|
||||
if let Err(err) = ConfigEditsBuilder::new(&config.codex_home)
|
||||
.set_model_availability_nux_count(&updated_shown_count)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -951,7 +951,7 @@ impl App {
|
||||
.codex_home(self.config.codex_home.clone())
|
||||
.cli_overrides(self.cli_kv_overrides.clone())
|
||||
.harness_overrides(overrides)
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to rebuild config for cwd {cwd_display}"))
|
||||
}
|
||||
@@ -1206,7 +1206,10 @@ impl App {
|
||||
// Persist first so the live session does not diverge from disk if the
|
||||
// config edit fails. Runtime/UI state is patched below only after the
|
||||
// durable config update succeeds.
|
||||
if let Err(err) = builder.apply().await {
|
||||
if let Err(err) = builder
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(error = %err, "failed to persist feature flags");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to update experimental features: {err}"));
|
||||
@@ -3270,7 +3273,10 @@ impl App {
|
||||
"unelevated"
|
||||
})
|
||||
.clear_legacy_windows_sandbox_keys();
|
||||
match builder.apply().await {
|
||||
match builder
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
if elevated_enabled {
|
||||
self.config.set_windows_sandbox_enabled(/*value*/ false);
|
||||
@@ -3364,7 +3370,7 @@ impl App {
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(profile)
|
||||
.set_model(Some(model.as_str()), effort)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -3405,7 +3411,7 @@ impl App {
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(profile)
|
||||
.set_personality(Some(personality))
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -3441,7 +3447,7 @@ impl App {
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(profile)
|
||||
.set_service_tier(service_tier)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -3480,7 +3486,10 @@ impl App {
|
||||
}
|
||||
};
|
||||
|
||||
match builder.apply().await {
|
||||
match builder
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
match kind {
|
||||
RealtimeAudioDeviceKind::Microphone => {
|
||||
@@ -3611,7 +3620,7 @@ impl App {
|
||||
segments,
|
||||
value: policy.to_string().into(),
|
||||
}])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -3646,7 +3655,7 @@ impl App {
|
||||
AppEvent::PersistFullAccessWarningAcknowledged => {
|
||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.set_hide_full_access_warning(/*acknowledged*/ true)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -3661,7 +3670,7 @@ impl App {
|
||||
AppEvent::PersistWorldWritableWarningAcknowledged => {
|
||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.set_hide_world_writable_warning(/*acknowledged*/ true)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -3676,7 +3685,7 @@ impl App {
|
||||
AppEvent::PersistRateLimitSwitchPromptHidden => {
|
||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.set_hide_rate_limit_model_nudge(/*acknowledged*/ true)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -3709,7 +3718,7 @@ impl App {
|
||||
};
|
||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits([edit])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -3733,7 +3742,7 @@ impl App {
|
||||
} => {
|
||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.record_model_migration_seen(from_model.as_str(), to_model.as_str())
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -3767,7 +3776,7 @@ impl App {
|
||||
}];
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits(edits)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -3819,7 +3828,7 @@ impl App {
|
||||
};
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits(edits)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -3939,7 +3948,7 @@ impl App {
|
||||
let edit = codex_core::config::edit::status_line_items_edit(&ids);
|
||||
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits([edit])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await;
|
||||
match apply_result {
|
||||
Ok(()) => {
|
||||
@@ -3965,7 +3974,7 @@ impl App {
|
||||
let edit = codex_core::config::edit::terminal_title_items_edit(&ids);
|
||||
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits([edit])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await;
|
||||
match apply_result {
|
||||
Ok(()) => {
|
||||
@@ -3991,7 +4000,7 @@ impl App {
|
||||
let edit = codex_core::config::edit::syntax_theme_edit(&name);
|
||||
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits([edit])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await;
|
||||
match apply_result {
|
||||
Ok(()) => {
|
||||
@@ -7145,7 +7154,7 @@ guardian_approval = true
|
||||
let codex_home = tempdir().expect("temp codex home");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.expect("config");
|
||||
|
||||
@@ -7243,7 +7252,7 @@ guardian_approval = true
|
||||
value: "user".into(),
|
||||
},
|
||||
])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.expect("persist app toggle");
|
||||
|
||||
|
||||
@@ -41,5 +41,12 @@ where
|
||||
}
|
||||
|
||||
pub async fn should_use_app_server_tui(cli: &Cli) -> std::io::Result<bool> {
|
||||
should_use_app_server_tui_with(cli, Config::load_with_cli_overrides_and_harness_overrides).await
|
||||
should_use_app_server_tui_with(cli, |cli_kv_overrides, config_overrides| {
|
||||
Config::load_with_cli_overrides_and_harness_overrides(
|
||||
cli_kv_overrides,
|
||||
config_overrides,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -328,6 +328,7 @@ pub async fn run_main(
|
||||
&codex_home,
|
||||
&config_cwd,
|
||||
cli_kv_overrides.clone(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1209,7 +1210,7 @@ async fn load_config_or_exit_with_fallback_cwd(
|
||||
.harness_overrides(overrides)
|
||||
.cloud_requirements(cloud_requirements)
|
||||
.fallback_cwd(fallback_cwd)
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
@@ -1268,7 +1269,7 @@ mod tests {
|
||||
async fn build_config(temp_dir: &TempDir) -> std::io::Result<Config> {
|
||||
ConfigBuilder::default()
|
||||
.codex_home(temp_dir.path().to_path_buf())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1462,7 +1463,7 @@ trust_level = "untrusted"
|
||||
let trusted_config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.clone())
|
||||
.harness_overrides(trusted_overrides.clone())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await?;
|
||||
assert_eq!(
|
||||
trusted_config.permissions.approval_policy.value(),
|
||||
@@ -1476,7 +1477,7 @@ trust_level = "untrusted"
|
||||
let untrusted_config = ConfigBuilder::default()
|
||||
.codex_home(codex_home)
|
||||
.harness_overrides(untrusted_overrides)
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await?;
|
||||
assert_eq!(
|
||||
untrusted_config.permissions.approval_policy.value(),
|
||||
|
||||
@@ -144,9 +144,34 @@ impl TrustDirectoryWidget {
|
||||
fn handle_trust(&mut self) {
|
||||
let target =
|
||||
resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone());
|
||||
if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) {
|
||||
tracing::error!("Failed to set project trusted: {e:?}");
|
||||
self.error = Some(format!("Failed to set trust for {}: {e}", target.display()));
|
||||
let codex_home = self.codex_home.clone();
|
||||
let target_for_task = target.clone();
|
||||
let target_display = target.display().to_string();
|
||||
let handle = match tokio::runtime::Handle::try_current() {
|
||||
Ok(handle) => handle,
|
||||
Err(err) => {
|
||||
self.error = Some(format!("Failed to set trust for {target_display}: {err}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
handle.spawn(async move {
|
||||
if let Err(err) = set_project_trust_level(
|
||||
&codex_home,
|
||||
&target_for_task,
|
||||
TrustLevel::Trusted,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
target = %target_for_task.display(),
|
||||
"Failed to set project trusted: {err:?}"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if self.error.is_some() {
|
||||
self.error = None;
|
||||
}
|
||||
|
||||
self.selection = Some(TrustDirectorySelection::Trust);
|
||||
|
||||
@@ -334,7 +334,12 @@ pub async fn select_oss_provider(codex_home: &std::path::Path) -> io::Result<Str
|
||||
// If the user manually selected an OSS provider, we save it as the
|
||||
// default one to use later.
|
||||
if let Ok(ref provider) = result
|
||||
&& let Err(e) = set_default_oss_provider(codex_home, provider)
|
||||
&& let Err(e) = set_default_oss_provider(
|
||||
codex_home,
|
||||
provider,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to save OSS provider preference: {e}");
|
||||
}
|
||||
|
||||
@@ -935,9 +935,12 @@ async fn resolve_auth() -> Result<TranscriptionAuthContext, String> {
|
||||
let token = auth
|
||||
.get_token()
|
||||
.map_err(|e| format!("failed to get auth token: {e}"))?;
|
||||
let config = Config::load_with_cli_overrides(Vec::new())
|
||||
.await
|
||||
.map_err(|e| format!("failed to load config: {e}"))?;
|
||||
let config = Config::load_with_cli_overrides(
|
||||
Vec::new(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("failed to load config: {e}"))?;
|
||||
Ok(TranscriptionAuthContext {
|
||||
mode: auth.api_auth_mode(),
|
||||
bearer_token: token,
|
||||
|
||||
@@ -41,6 +41,7 @@ codex-chatgpt = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-feedback = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
|
||||
@@ -799,7 +799,7 @@ async fn prepare_startup_tooltip_override(
|
||||
|
||||
if let Err(err) = ConfigEditsBuilder::new(&config.codex_home)
|
||||
.set_model_availability_nux_count(&updated_shown_count)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -1043,7 +1043,7 @@ impl App {
|
||||
.codex_home(self.config.codex_home.clone())
|
||||
.cli_overrides(self.cli_kv_overrides.clone())
|
||||
.harness_overrides(overrides)
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to rebuild config for cwd {cwd_display}"))
|
||||
}
|
||||
@@ -1298,7 +1298,10 @@ impl App {
|
||||
// Persist first so the live session does not diverge from disk if the
|
||||
// config edit fails. Runtime/UI state is patched below only after the
|
||||
// durable config update succeeds.
|
||||
if let Err(err) = builder.apply().await {
|
||||
if let Err(err) = builder
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(error = %err, "failed to persist feature flags");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to update experimental features: {err}"));
|
||||
@@ -4002,7 +4005,10 @@ impl App {
|
||||
"unelevated"
|
||||
})
|
||||
.clear_legacy_windows_sandbox_keys();
|
||||
match builder.apply().await {
|
||||
match builder
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
if elevated_enabled {
|
||||
self.config.set_windows_sandbox_enabled(/*value*/ false);
|
||||
@@ -4100,7 +4106,7 @@ impl App {
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(profile)
|
||||
.set_model(Some(model.as_str()), effort)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -4141,7 +4147,7 @@ impl App {
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(profile)
|
||||
.set_personality(Some(personality))
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -4177,7 +4183,7 @@ impl App {
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(profile)
|
||||
.set_service_tier(service_tier)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -4216,7 +4222,10 @@ impl App {
|
||||
}
|
||||
};
|
||||
|
||||
match builder.apply().await {
|
||||
match builder
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
match kind {
|
||||
RealtimeAudioDeviceKind::Microphone => {
|
||||
@@ -4347,7 +4356,7 @@ impl App {
|
||||
segments,
|
||||
value: policy.to_string().into(),
|
||||
}])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -4382,7 +4391,7 @@ impl App {
|
||||
AppEvent::PersistFullAccessWarningAcknowledged => {
|
||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.set_hide_full_access_warning(/*acknowledged*/ true)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -4397,7 +4406,7 @@ impl App {
|
||||
AppEvent::PersistWorldWritableWarningAcknowledged => {
|
||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.set_hide_world_writable_warning(/*acknowledged*/ true)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -4412,7 +4421,7 @@ impl App {
|
||||
AppEvent::PersistRateLimitSwitchPromptHidden => {
|
||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.set_hide_rate_limit_model_nudge(/*acknowledged*/ true)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -4445,7 +4454,7 @@ impl App {
|
||||
};
|
||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits([edit])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -4469,7 +4478,7 @@ impl App {
|
||||
} => {
|
||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.record_model_migration_seen(from_model.as_str(), to_model.as_str())
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -4503,7 +4512,7 @@ impl App {
|
||||
}];
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits(edits)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -4555,7 +4564,7 @@ impl App {
|
||||
};
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits(edits)
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -4675,7 +4684,7 @@ impl App {
|
||||
let edit = codex_core::config::edit::status_line_items_edit(&ids);
|
||||
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits([edit])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await;
|
||||
match apply_result {
|
||||
Ok(()) => {
|
||||
@@ -4700,7 +4709,7 @@ impl App {
|
||||
let edit = codex_core::config::edit::syntax_theme_edit(&name);
|
||||
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits([edit])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await;
|
||||
match apply_result {
|
||||
Ok(()) => {
|
||||
@@ -8421,7 +8430,7 @@ guardian_approval = true
|
||||
let codex_home = tempdir().expect("temp codex home");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.expect("config");
|
||||
|
||||
@@ -8519,7 +8528,7 @@ guardian_approval = true
|
||||
value: "user".into(),
|
||||
},
|
||||
])
|
||||
.apply()
|
||||
.apply(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
.expect("persist app toggle");
|
||||
|
||||
|
||||
@@ -654,6 +654,7 @@ pub async fn run_main(
|
||||
&codex_home,
|
||||
&config_cwd,
|
||||
cli_kv_overrides.clone(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1546,7 +1547,7 @@ async fn load_config_or_exit_with_fallback_cwd(
|
||||
.harness_overrides(overrides)
|
||||
.cloud_requirements(cloud_requirements)
|
||||
.fallback_cwd(fallback_cwd)
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
@@ -1608,7 +1609,7 @@ mod tests {
|
||||
async fn build_config(temp_dir: &TempDir) -> std::io::Result<Config> {
|
||||
ConfigBuilder::default()
|
||||
.codex_home(temp_dir.path().to_path_buf())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1965,7 +1966,7 @@ trust_level = "untrusted"
|
||||
let trusted_config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.clone())
|
||||
.harness_overrides(trusted_overrides.clone())
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await?;
|
||||
assert_eq!(
|
||||
trusted_config.permissions.approval_policy.value(),
|
||||
@@ -1979,7 +1980,7 @@ trust_level = "untrusted"
|
||||
let untrusted_config = ConfigBuilder::default()
|
||||
.codex_home(codex_home)
|
||||
.harness_overrides(untrusted_overrides)
|
||||
.build()
|
||||
.build(codex_exec_server::Environment::default().get_filesystem())
|
||||
.await?;
|
||||
assert_eq!(
|
||||
untrusted_config.permissions.approval_policy.value(),
|
||||
|
||||
@@ -144,9 +144,34 @@ impl TrustDirectoryWidget {
|
||||
fn handle_trust(&mut self) {
|
||||
let target =
|
||||
resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone());
|
||||
if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) {
|
||||
tracing::error!("Failed to set project trusted: {e:?}");
|
||||
self.error = Some(format!("Failed to set trust for {}: {e}", target.display()));
|
||||
let codex_home = self.codex_home.clone();
|
||||
let target_for_task = target.clone();
|
||||
let target_display = target.display().to_string();
|
||||
let handle = match tokio::runtime::Handle::try_current() {
|
||||
Ok(handle) => handle,
|
||||
Err(err) => {
|
||||
self.error = Some(format!("Failed to set trust for {target_display}: {err}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
handle.spawn(async move {
|
||||
if let Err(err) = set_project_trust_level(
|
||||
&codex_home,
|
||||
&target_for_task,
|
||||
TrustLevel::Trusted,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
target = %target_for_task.display(),
|
||||
"Failed to set project trusted: {err:?}"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if self.error.is_some() {
|
||||
self.error = None;
|
||||
}
|
||||
|
||||
self.selection = Some(TrustDirectorySelection::Trust);
|
||||
|
||||
@@ -334,7 +334,12 @@ pub async fn select_oss_provider(codex_home: &std::path::Path) -> io::Result<Str
|
||||
// If the user manually selected an OSS provider, we save it as the
|
||||
// default one to use later.
|
||||
if let Ok(ref provider) = result
|
||||
&& let Err(e) = set_default_oss_provider(codex_home, provider)
|
||||
&& let Err(e) = set_default_oss_provider(
|
||||
codex_home,
|
||||
provider,
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to save OSS provider preference: {e}");
|
||||
}
|
||||
|
||||
@@ -773,9 +773,12 @@ async fn resolve_auth() -> Result<TranscriptionAuthContext, String> {
|
||||
let token = auth
|
||||
.get_token()
|
||||
.map_err(|e| format!("failed to get auth token: {e}"))?;
|
||||
let config = Config::load_with_cli_overrides(Vec::new())
|
||||
.await
|
||||
.map_err(|e| format!("failed to load config: {e}"))?;
|
||||
let config = Config::load_with_cli_overrides(
|
||||
Vec::new(),
|
||||
codex_exec_server::Environment::default().get_filesystem(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("failed to load config: {e}"))?;
|
||||
Ok(TranscriptionAuthContext {
|
||||
mode: auth.api_auth_mode(),
|
||||
bearer_token: token,
|
||||
|
||||
Reference in New Issue
Block a user